Design Patterns
What is a design Pattern ?
An established solution to the most commonly encountered problems in software design.
Creational Design Pattern
Creational Design Pattern is concerned with the way in which the objects are being created.
- Singleton
- Factory Method
- Abstract Factory
- Builder
Singleton
The Singleton Design Pattern aims to keep a check on initialization of objects of a particular class by ensuring that only one instance of the object exists throughout the Java Virtual Machine.
package com.designPattern.demo;
class DBConnection {
static DBConnection redisConnection;
int port;
String database;
private DBConnection() {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.port = 8010;
this.database = "Redis";
}
static DBConnection getInstance() {
if (redisConnection == null) {
redisConnection = new DBConnection();
}
return redisConnection;
}
@Override
public String toString() {
return "DBConnection{" +
"port=" + port +
", database='" + database + '\'' +
'}';
}
}
public class Singleton {
public static void main(String[] args) {
System.out.println(DBConnection.getInstance());
System.out.println(DBConnection.getInstance());
}
}
When to use singleton design pattern
- For resources that are expensive to create (like database connection objects)
- It’s good practice to keep all loggers as Singletons which increases performance
- Classes which provide access to configuration settings for the application
- Classes that contain resources that are accessed in shared mode
Factory Design Pattern
- Factory design pattern is used when we have a super class with multiple sub-classes and based on input, we need to return one of the sub-class.
- This pattern take out the responsibility of instantiation of a class from client program to the factory class.
package com.designPattern.demo;
interface HotDrink{
void prepareHotDrink();
}
class Tea implements HotDrink{
@Override
public void prepareHotDrink() {
System.out.println("This restaurant will serve Tea");
}
}
class Coffee implements HotDrink{
@Override
public void prepareHotDrink() {
System.out.println("This restaurant will serve coffee");
}
}
class Restaurant{
HotDrink hotDrink;
public HotDrink getHotDrink() {
return hotDrink;
}
public void setHotDrink(HotDrink hotDrink) {
this.hotDrink = hotDrink;
}
}
class RestaurantFactory{
static Restaurant getRestaurantObject(String name){
Restaurant restaurant= new Restaurant();
switch (name){
case "restaurantWithTea":
restaurant.setHotDrink(new Tea());
break;
case "restaurantWithCoffee" :
restaurant.setHotDrink(new Coffee());
break;
}
return restaurant;
}
}
public class FactoryPattern {
public static void main(String[] args) {
Restaurant restaurantWithTea = RestaurantFactory.getRestaurantObject("restaurantWithTea");
restaurantWithTea.getHotDrink().prepareHotDrink();
Restaurant restaurantWithCoffee = RestaurantFactory.getRestaurantObject("restaurantWithCoffee");
restaurantWithCoffee.getHotDrink().prepareHotDrink();
}
}
When to Use Factory Method Design Pattern
- When the implementation of an interface or an abstract class is expected to change frequently
- When the current implementation cannot comfortably accommodate new change
- When the initialization process is relatively simple, and the constructor only requires a handful of parameters
Abstract Factory
- Abstract Factory patterns work around a super-factory which creates other factories.
- This factory is also called as factory of factories.
enum CarType {
MICRO, MINI, LUXURY;
}
enum Location {
DEFAULT, USA, INDIA;
}
abstract class Car {
CarType carType;
Location location;
public Car(CarType carType, Location location) {
this.carType = carType;
this.location = location;
}
abstract void construct();
@Override
public String toString() {
return "Car{" +
"carType=" + carType +
", location=" + location +
'}';
}
}
class LuxuryCar extends Car {
public LuxuryCar(Location location) {
super(CarType.LUXURY, location);
}
@Override
void construct() {
System.out.println("connecting to Luxury Car");
}
}
class MiniCar extends Car {
public MiniCar(Location location) {
super(CarType.MINI, location);
}
@Override
void construct() {
System.out.println("connecting to Mini Car");
}
}
class MicroCar extends Car {
public MicroCar(Location location) {
super(CarType.MINI, location);
}
@Override
void construct() {
System.out.println("connecting to Micro Car");
}
}
class IndianCarFactory {
static Car buildCar(CarType carType) {
Car car = null;
switch (carType) {
case MICRO:
car = new MicroCar(Location.INDIA);
break;
case MINI:
car = new MiniCar(Location.INDIA);
break;
case LUXURY:
car = new LuxuryCar(Location.INDIA);
break;
}
return car;
}
}
class DefaultCarFactory {
static Car buildCar(CarType carType) {
Car car = null;
switch (carType) {
case MICRO:
car = new MicroCar(Location.DEFAULT);
break;
case MINI:
car = new MiniCar(Location.DEFAULT);
break;
case LUXURY:
car = new LuxuryCar(Location.DEFAULT);
break;
}
return car;
}
}
class USACarFactory {
static Car buildCar(CarType carType) {
Car car = null;
switch (carType) {
case MICRO:
car = new MicroCar(Location.USA);
break;
case MINI:
car = new MiniCar(Location.USA);
break;
case LUXURY:
car = new LuxuryCar(Location.USA);
break;
}
return car;
}
}
class CarFactory {
Car car = null;
static Car buildCar(CarType carType, Location location) {
Car car = null;
switch (location) {
case INDIA:
car = IndianCarFactory.buildCar(carType);
break;
case USA:
car = USACarFactory.buildCar(carType);
break;
case DEFAULT:
car = DefaultCarFactory.buildCar(carType);
break;
}
return car;
}
}
public class AbstractFactory {
public static void main(String[] args) {
System.out.println(
CarFactory.buildCar(CarType.MICRO, Location.DEFAULT)
);
}
}
When to Use Abstract Factory Pattern:
- The client should be independent of how the products are created and composed in the system
- The system consists of multiple families of products, and these families are designed to be used together
- We need a run-time value to construct a particular dependency
Builder
When the complexity of creating object increases, the Builder pattern can separate out the instantiation process by using another object (a builder) to construct the object.
class Employee {
private String name;
private Integer id;
private Integer salary;
private boolean optedForPF;
private boolean isOptedForNPS;
private Integer contributionToDonation;
public Employee(EmployeeBuilder employeeBuilder) {
id = employeeBuilder.getId();
name = employeeBuilder.getName();
salary = employeeBuilder.getSalary();
optedForPF = employeeBuilder.isOptedForPF();
isOptedForNPS = employeeBuilder.isOptedForNPS();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getSalary() {
return salary;
}
public void setSalary(Integer salary) {
this.salary = salary;
}
public boolean isOptedForPF() {
return optedForPF;
}
public void setOptedForPF(boolean optedForPF) {
this.optedForPF = optedForPF;
}
public boolean isOptedForNPS() {
return isOptedForNPS;
}
public void setOptedForNPS(boolean optedForNPS) {
isOptedForNPS = optedForNPS;
}
public Integer getContributionToDonation() {
return contributionToDonation;
}
public void setContributionToDonation(Integer contributionToDonation) {
this.contributionToDonation = contributionToDonation;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
", id=" + id +
", salary=" + salary +
", optedForPF=" + optedForPF +
", isOptedForNPS=" + isOptedForNPS +
", contributionToDonation=" + contributionToDonation +
'}';
}
}
class EmployeeBuilder {
private String name;
private Integer id;
private Integer salary;
private boolean optedForPF;
private boolean isOptedForNPS;
private Integer contributionToDonation;
public EmployeeBuilder(String name, Integer id) {
this.name = name;
this.id = id;
}
public String getName() {
return name;
}
public EmployeeBuilder setName(String name) {
this.name = name;
return this;
}
public Integer getId() {
return id;
}
public EmployeeBuilder setId(Integer id) {
this.id = id;
return this;
}
public Integer getSalary() {
return salary;
}
public EmployeeBuilder withSalary(Integer salary) {
this.salary = salary;
return this;
}
public boolean isOptedForPF() {
return optedForPF;
}
public EmployeeBuilder hasOptedForPF(boolean optedForPF) {
this.optedForPF = optedForPF;
return this;
}
public boolean isOptedForNPS() {
return isOptedForNPS;
}
public EmployeeBuilder hasOptedForNPS(boolean optedForNPS) {
isOptedForNPS = optedForNPS;
return this;
}
public Integer getContributionToDonation() {
return contributionToDonation;
}
public EmployeeBuilder willContributionAmountToDonation(Integer contributionToDonation) {
this.contributionToDonation = contributionToDonation;
return this;
}
public Employee build() {
return new Employee(this);
}
}
public class Builder {
public static void main(String[] args) {
Employee employee = new EmployeeBuilder("Pulkit",24)
.withSalary(100000)
.willContributionAmountToDonation(2000)
.hasOptedForNPS(true)
.hasOptedForPF(false)
.build();
System.out.println(employee);
}
}
When to Use Builder Pattern
- When the process involved in creating an object is extremely complex, with lots of mandatory and optional parameters
- When an increase in the number of constructor parameters leads to a large list of constructors
- When client expects different representations for the object that’s constructed
Structure Design Pattern
Structural Design Pattern are concerned with how classes and Objects can be composed to form a large structure
- Bridge
- Decorator
- Composite
- Proxy
Bridge
- Decouple an abstraction from its implementation so that the two can vary independently.
-
This means to create a bridge interface that uses OOP principles to separate out responsibilities into different abstract classes.
Elements of Bridge Design Pattern
- Abstraction : core of the bridge design pattern and defines the crux. Contains a reference to the implementer.
- Refined Abstraction : Extends the abstraction takes the finer detail one level below.
- Implementor : It defines the interface for implementation classes.
- Concrete Implementation : Implements the above implementer by providing concrete implementation.
Without Bridge Design pattern
Vehicle |
---|
Bus |
---|
Bike |
---|
ProducedBike |
---|
AssembledBike |
---|
ProducedBus |
---|
AssembledBus |
---|
With Bridge Design Pattern
Vehicle |
---|
Workshop |
---|
Bus |
---|
Bike |
---|
Produce |
---|
Assemble |
---|
// Implementor
interface Workshop {
String getWorkshopType();
}
// Concrete Implementor
class Assemble implements Workshop{
@Override
public String getWorkshopType() {
return "Assemble";
}
}
// Concrete Implementor
class Manufacture implements Workshop{
@Override
public String getWorkshopType() {
return "Manufacture";
}
}
// Abstraction
abstract class Vehicle {
String type;
Workshop workshop;
public Vehicle(String type, Workshop workshop) {
this.type = type;
this.workshop = workshop;
}
}
// Refined Abstraction
class Bike extends Vehicle {
String specification;
public Bike(String specification, Workshop workshop) {
super("2 Wheeler", workshop);
this.specification = specification;
}
@Override
public String toString() {
return "Bike{" +
"type='" + type + '\'' +
", workshop=" + workshop.getWorkshopType() +
", specification='" + specification + '\'' +
'}';
}
}
// Refined Abstraction
class Bus extends Vehicle {
Integer seats;
public Bus(Integer seats, Workshop workshop) {
super("3 Wheeler", workshop);
this.seats = seats;
}
@Override
public String toString() {
return "Bus{" +
"type='" + type + '\'' +
", workshop=" + workshop.getWorkshopType() +
", seats=" + seats +
'}';
}
}
class BridgePattern {
public static void main(String[] args) {
Bike avenger = new Bike("Cruiser", new Assemble());
Bike passion = new Bike("Regular", new Manufacture());
System.out.println(avenger);
System.out.println(passion);
Bus bus = new Bus(30, new Manufacture());
System.out.println(bus);
}
}
When to Use Bridge Design Pattern
- When we want a parent abstract class to define the set of basic rules, and the concrete classes to add additional rules.
- When we have an abstract class that has a reference to the objects, and it has abstract methods that will be defined in each of the concrete classes.
Decorator
- A Decorator pattern can be used to attach additional responsibilities to an object either statically or dynamically.
- A Decorator provides an enhanced interface to the original object.
- we prefer composition over an inheritance.
interface Shape {
String info();
}
class Circle implements Shape {
private Integer radius;
public Circle(Integer radius) {
this.radius = radius;
}
public Integer getRadius() {
return radius;
}
public void setRadius(Integer radius) {
this.radius = radius;
}
@Override
public String info() {
return "A circle with radius : " + radius;
}
}
class Square implements Shape {
private Integer length;
private Integer breadth;
public Square(Integer length, Integer breadth) {
this.length = length;
this.breadth = breadth;
}
public Integer getLength() {
return length;
}
public void setLength(Integer length) {
this.length = length;
}
public Integer getBreadth() {
return breadth;
}
public void setBreadth(Integer breadth) {
this.breadth = breadth;
}
@Override
public String info() {
return "A square with length : " + length + " breadth : " + breadth;
}
}
class ColoredShape implements Shape {
private Shape shape;
private String color;
public ColoredShape(Shape shape, String color) {
this.shape = shape;
this.color = color;
}
public Shape getShape() {
return shape;
}
public void setShape(Shape shape) {
this.shape = shape;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
@Override
public String info() {
return shape.info() + " with color : " + color;
}
}
class TransparentShape implements Shape {
private Shape shape;
private Integer transparency;
public TransparentShape(Shape shape, Integer transparency) {
this.shape = shape;
this.transparency = transparency;
}
public Shape getShape() {
return shape;
}
public void setShape(Shape shape) {
this.shape = shape;
}
public Integer getTransparency() {
return transparency;
}
public void setTransparency(Integer transparency) {
this.transparency = transparency;
}
@Override
public String info() {
return shape.info() + " with transparency : " + transparency;
}
}
public class Decorator {
public static void main(String[] args) {
Square square= new Square(3,4);
System.out.println(square.info());
ColoredShape coloredShape = new ColoredShape(
new Square(5,7),
"Blue");
System.out.println(coloredShape.info());
TransparentShape transparentShape = new TransparentShape(
new ColoredShape(new Circle(5),"pink"),50);
System.out.println(transparentShape.info());
}
}
When to Use Decorator Pattern
- When we wish to add, enhance or even remove the behavior or state of objects
- When we just want to modify the functionality of a single object of class and leave others unchanged
Composite
- The composite pattern makes sense only when your business model can be represented as a tree.
- A group of objects that is treated the same way as a single instance of the same type of object.
The Composite Pattern has four participants:
- Component : Component declares the interface for objects in the composition and for accessing and managing its child component.
- Leaf : Leaf defines behavior for primitive objects in the composition. It represents leaf objects in the composition.
- Composite : Composite stores child components and implements child related operations in the component interface.
- Client : Client manipulates the objects in the composition through the component interfac
//Component
interface Directory {
void showDirectoryDetails();
}
//Leaf
class Developer implements Directory {
private String name;
private String favoriteLanguage;
public Developer(String name, String favoriteLanguage) {
this.name = name;
this.favoriteLanguage = favoriteLanguage;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getFavoriteLanguage() {
return favoriteLanguage;
}
public void setFavoriteLanguage(String favoriteLanguage) {
this.favoriteLanguage = favoriteLanguage;
}
@Override
public void showDirectoryDetails() {
System.out.println("Developer{" +
"name='" + name + '\'' +
", favoriteLanguage='" + favoriteLanguage + '\'' +
'}');
}
@Override
public String toString() {
return "Developer{" +
"name='" + name + '\'' +
", favoriteLanguage='" + favoriteLanguage + '\'' +
'}';
}
}
//leaf
class Manager implements Directory{
private String name;
private String type;
public Manager(String name, String type) {
this.name = name;
this.type = type;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
@Override
public void showDirectoryDetails() {
System.out.println("Manager{" +
"name='" + name + '\'' +
", type='" + type + '\'' +
'}');
}
@Override
public String toString() {
return "Manager{" +
"name='" + name + '\'' +
", type='" + type + '\'' +
'}';
}
}
//Composite
class DeveloperDirectory implements Directory{
List<Directory> developerDirectory = new ArrayList<Directory>();
@Override
public void showDirectoryDetails() {
System.out.println("Developer Directory");
developerDirectory.forEach(e->e.showDirectoryDetails());
}
}
//Composite
class ManagerDirectory implements Directory{
List<Directory> managerDirectory = new ArrayList<Directory>();
@Override
public void showDirectoryDetails() {
System.out.println("Manager Directory");
managerDirectory.forEach(e->e.showDirectoryDetails());
}
}
//Composite
class CompanyDirectory implements Directory{
List<Directory> companyDirectory = new ArrayList<>();
@Override
public void showDirectoryDetails() {
companyDirectory.forEach(e->e.showDirectoryDetails());
}
}
//Client
public class Composite {
public static void main(String[] args) {
Developer developer1 = new Developer("John","Java");
Developer developer2 = new Developer("Sam","C#");
Manager manager1 = new Manager("Devin","Associate");
Manager manager2 = new Manager("James","Senior");
DeveloperDirectory developerDirectory= new DeveloperDirectory();
developerDirectory.developerDirectory.add(developer1);
developerDirectory.developerDirectory.add(developer2);
ManagerDirectory managerDirectory = new ManagerDirectory();
managerDirectory.managerDirectory.add(manager1);
managerDirectory.managerDirectory.add(manager2);
CompanyDirectory companyDirectory = new CompanyDirectory();
companyDirectory.companyDirectory.add(developerDirectory);
companyDirectory.companyDirectory.add(managerDirectory);
companyDirectory.showDirectoryDetails();
}
}
Proxy
- lets you provide a substitute or placeholder for another object to control access to it.
class Driver{
private int age;
public Driver(int age) {
this.age = age;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
interface Drivable{
void drive();
}
class Car implements Drivable{
protected Driver driver;
public Car(Driver driver) {
this.driver = driver;
}
@Override
public void drive() {
System.out.println("drive the car");
}
}
class CarProxy extends Car{
public CarProxy(Driver driver) {
super(driver);
}
@Override
public void drive() {
if(driver.getAge()<21){
System.out.println("You can't drive the car");
}else {
System.out.println("you can drive");
super.drive();
}
}
}
public class Proxy {
public static void main(String[] args) {
Driver john= new Driver(29);
Drivable car= new CarProxy(john);
car.drive();
}
}
When to Use Proxy
When we want to add a layer of security to the original underlying object to provide controlled access based on access rights of the client.
Exercise
- Implement Singleton Design Pattern on a dummy class.
- Implement Factory Pattern to get the Polygon of differnt type.
- Implement Abstract Factory Pattern to create cars of different categories from different countries.
- Implement Builder pattern to create a student object with more than 6 fields.
- Implement Bridge Design Pattern for Color and Shape such that Shape and Color can be combined together e.g BlueSquare, RedSquare, PinkTriangle etc.
- Implement Decorator pattern to decorate the Pizza with topings.
- Implement Composite Design Pattern to maintaing the directories of employees on the basis of departments.
- Implement proxy design for accessing Record of a student and allow the access only to Admin.
Design Patterns
By Pulkit Pushkarna
Design Patterns
- 1,666