DEV Community

Cover image for Understanding Creational Design Patterns: A Comprehensive Guide
Suman Pal
Suman Pal

Posted on • Updated on

Understanding Creational Design Patterns: A Comprehensive Guide

Creational Design Pattern

Creational Design Patterns are a distinct category of design patterns that focus on the creation of objects and their associated mechanisms. These patterns offer techniques for object creation that conceal the underlying intricacies of instantiation and initialization. The primary objective of creational patterns is to enhance the flexibility, maintainability, and class independence of a system.


This article aims to present a detailed explanation of six creational design patterns with respective UML and Sequence Diagrams.

Singleton Design Pattern :

Definition - Ensures that a class has only one instance and provides a global point of access to that instance. It's useful when exactly one object is needed.

Why/When to use

  • Single Instance Requirement: Use the Singleton pattern when you need to ensure that a class has only one instance throughout the application's lifetime.
  • Global Access: When multiple parts of your application need access to a single instance of an object, the Singleton provides a global point of access.
  • Resource Management: It's suitable for managing resources like database connections, logging, thread pools, or caches to avoid excessive resource allocation.

Benefits

  • Single Instance: Ensures that a class has only one instance throughout the application, which can be accessed globally.
  • Global Access: Provides a single point of access to the instance, making it easy to share data or coordinate actions across the system.
  • Resource Efficiency: Lazily initializes the instance, conserving resources until it's actually needed.
  • Thread Safety: Can be designed to be thread-safe, preventing issues with multiple threads accessing the instance simultaneously.

UML -
Singleton UML

Code -

class SingletonClass {
    private static SingletonClass singletonInstance;
    private SingletonClass(){
         System.out.println("Instance created");
    }
    public static SingletonClass getInstance(){
        if(singletonInstance == null){
            singletonInstance = new SingletonClass();
        }
        return singletonInstance;
    }
    public void simpleMethod(){
        System.out.println("hashcode of singelton object     " + singletonInstance);
    }
}

public class Client {
    public static void main(String[] args) {
        SingletonClass singletonClassObject = SingletonClass.getInstance();
        singletonClassObject.simpleMethod();

        SingletonClass singletonClassObject2 = SingletonClass.getInstance();
        singletonClassObject2.simpleMethod();
    }
}
Enter fullscreen mode Exit fullscreen mode

Prototype Design Pattern :

Definition - Creates new objects by copying an existing object, known as a prototype. This pattern is useful when the cost of creating an object is more expensive or complex than copying an existing one.

Why/When to use -

  • Object Cloning: When you want to create new objects by copying an existing object, the Prototype pattern is valuable. This is especially useful when the cost of creating an object is high.
  • Customization of Objects: If you need to create similar objects with minor variations, the Prototype allows you to clone and customize objects as needed.
  • Performance Optimization: In situations where cloning is more efficient than creating objects from scratch, such as with large or complex objects, the Prototype pattern can improve performance.

Benefits

  • Object Cloning: Creates new objects by copying an existing object, reducing the overhead of creating complex objects from scratch.
  • Customization: Allows customization of copied objects by modifying their attributes as needed.
  • Performance: Improves performance when object creation is more costly than cloning.

UML -
Prototype UML

Code -

import java.util.List;
import java.util.ArrayList;

class Employees implements Cloneable {
    private List<String> employeeList;
    public Employees(){
        employeeList = new ArrayList<>();
    }
    public Employees(List<String> empList){
        this.employeeList = empList;
    }
    public List<String> getEmployeeList(){
        return employeeList;
    }
    public void setEmployeeList(List<String> empsList){
        this.employeeList = empsList;
    }
    public void loadEmployeeData(){
        employeeList.add("Tom");
        employeeList.add("David");
        employeeList.add("Steve");
    }

    @Override
    public Object clone() throws CloneNotSupportedException{
        List<String> temp = new ArrayList<>();
        for(String s : this.getEmployeeList()){
            temp.add(s);
        }

        return new Employees(temp);
    }
}

public class PrototypePattern{
    public static void main (String[] args) throws CloneNotSupportedException {
        Employees emps =  new Employees();
        emps.loadEmployeeData();

        //Use the clone method to get the Employee Object
        Employees empsNew = (Employees) emps.clone();
        Employees empsNew2 = (Employees) emps.clone();

        //modifying empsNew
        List<String> list = empsNew.getEmployeeList();
        list.add("Roger");
        empsNew.setEmployeeList(list);
        //modifying empsNew2
        List<String> list2 = empsNew2.getEmployeeList();
        list2.remove("David");
        empsNew2.setEmployeeList(list2);

        System.out.println("emps List : " + emps.getEmployeeList());
        System.out.println("empsNew List : " + empsNew.getEmployeeList());
        System.out.println("empsNew2 List : " + empsNew2.getEmployeeList());
    }
}
Enter fullscreen mode Exit fullscreen mode

Builder Design Pattern :

Definition - It lets you construct complex objects step by step. The pattern allows you to produce different types and representations of an object using the same construction code.

Why/When to use -

  • Complex Object Construction: When object creation involves multiple steps, complex configurations, or optional parameters, the Builder pattern simplifies the construction process.
  • Fluent Interface: Use Builder when you want to provide a fluent and expressive way for clients to configure and build objects by chaining method calls.
  • Immutability: Builders often create immutable objects, making them suitable for scenarios where the object's state should not change after creation.

Benefits

  • Complex Object Construction: Separates the construction of a complex object from its representation, allowing for more control and flexibility in object creation.
  • Fluent Interface: Often implemented with a fluent interface, making code more readable and expressive when configuring objects.
  • Parameterization: Provides a way to construct objects with different configurations, reducing the need for multiple constructor overloads.
  • Choose what you need: When we have lots of attributes in a class, and we don't want every attribute every time. So we should be free to use any attribute based on our use requirement.

UML -
Builder UML

Code -

interface SkillsRating{
    public void setJava(int java);
    public void setDSA(int dsa);
    public void setSelenium(int selenium);
    public void setSpringBoot(int springBoot);
    public void setAWS(int aws);
    public void setReact(int react);
}

class SoftwareEngineer implements SkillsRating{
    private int java;
    private int dsa;
    private int selenium;
    private int springBoot;
    private int aws;
    private int react;

    public void setJava(int java){
        this.java=java;
    }
    public void setDSA(int dsa){
        this.dsa=dsa;
    }
    public void setSelenium(int selenium){
        this.selenium=selenium;
    }
    public void setSpringBoot(int springBoot){
        this.springBoot=springBoot;
    }
    public void setAWS(int aws){
        this.aws=aws;
    }
    public void setReact(int react){
        this.react=react;
    }
    public void showSkillsDetails(String role){
        System.out.println(role + " =>  Java : " + java + "| DSA : " + dsa + "| Selenium : " + selenium +
                            "| SpringBoot : " + springBoot + "| AWS : " + aws + "| React : " + react);
    }
}

interface SkillsBuilder{
    public SkillsBuilder setJavaSkill(int java);
    public SkillsBuilder setDSASkill(int dsa);
    public SkillsBuilder setSeleniumSkill(int selenium);
    public SkillsBuilder setSpringBootSkill(int springBoot);
    public SkillsBuilder setAWSSkill(int aws);
    public SkillsBuilder setReactSkill(int react);
    public SoftwareEngineer getSoftwareEngineer();
}

class SoftwareDeveloperBuilder implements SkillsBuilder{
    private SoftwareEngineer softwareEngineer;

    public SoftwareDeveloperBuilder()   {
        this.softwareEngineer = new SoftwareEngineer();
    }

    public SkillsBuilder setJavaSkill(int java){
        this.softwareEngineer.setJava(java);
        return this;
    }
    public SkillsBuilder setDSASkill(int dsa){
        this.softwareEngineer.setDSA(dsa);
        return this;
    }
    public SkillsBuilder setSeleniumSkill(int selenium){
        this.softwareEngineer.setSelenium(selenium);
        return this;
    }
    public SkillsBuilder setSpringBootSkill(int springBoot){
        this.softwareEngineer.setSpringBoot(springBoot);
        return this;
    }
    public SkillsBuilder setAWSSkill(int aws){
        this.softwareEngineer.setAWS(aws);
        return this;
    }
    public SkillsBuilder setReactSkill(int react){
        this.softwareEngineer.setReact(react);
        return this;
    }

    public SoftwareEngineer getSoftwareEngineer(){
        return this.softwareEngineer;
    }
}

class Client {
    public static void main(String[] args)  {
        SkillsBuilder backendDeveloperBuilder = new SoftwareDeveloperBuilder();
        SoftwareEngineer backendDeveloper = backendDeveloperBuilder.setJavaSkill(8).setSpringBootSkill(7).setDSASkill(9).setAWSSkill(5).getSoftwareEngineer();

        SkillsBuilder frontendDeveloperBuilder = new SoftwareDeveloperBuilder();
        SoftwareEngineer frontendDeveloper = frontendDeveloperBuilder.setReactSkill(8).setDSASkill(7).setAWSSkill(5).getSoftwareEngineer();

        SkillsBuilder devOpsBuilder = new SoftwareDeveloperBuilder();
        SoftwareEngineer devOps = devOpsBuilder.setDSASkill(6).setAWSSkill(9).getSoftwareEngineer();

        SkillsBuilder testerBuilder = new SoftwareDeveloperBuilder();
        SoftwareEngineer tester = testerBuilder.setSeleniumSkill(9).setDSASkill(7).setJavaSkill(7).setAWSSkill(5).getSoftwareEngineer();

        backendDeveloper.showSkillsDetails("BackendDeveloper");
        devOps.showSkillsDetails("DevOps");
        frontendDeveloper.showSkillsDetails("FrontendDeveloper");
        tester.showSkillsDetails("Tester");
    }
}
Enter fullscreen mode Exit fullscreen mode

Factory Method Design Pattern :

Definition - Defines an interface for creating an object, but lets subclasses alter the type of objects that will be created.

Why/When to use -

  • Polymorphism and Extensibility: When you want to provide a way for subclasses to determine the type of objects they create, use the Factory Method pattern to promote polymorphism and extensibility.
  • Decoupling: If you need to decouple the client code from the specific class being instantiated, Factory Methods provide a level of indirection.
  • Configurability: When you want to allow clients to configure the objects they create by providing parameters or options, the Factory Method pattern is helpful.

Benefits

  • Flexibility: Allows subclasses to determine the type of objects that will be created, promoting flexibility in object creation.
  • Polymorphism: Supports polymorphism by returning objects of a common interface, making it easy to work with different implementations.
  • Separation of Concerns: Decouples the client code from the concrete class being instantiated, improving maintainability and testability.

UML -
Factory UML

Code -

interface Profession {
    void print();
}

class Teacher implements Profession{
    public void print() {
        System.out.println("In Print of Teacher class");
    }
}

class Doctor implements Profession{
    public void print() {
        System.out.println("In Print of Doctor class");
    }
}

class ProfessionFactory {
    public Profession getProfession(String typeOfProfession){
        Profession prof=null;
          if(typeOfProfession.equalsIgnoreCase("Doctor")){
              prof=new Doctor();
          }else if(typeOfProfession.equalsIgnoreCase("Teacher")){
              prof=new Teacher();
          }
          return prof;
       }
}

public class Client {
    public static void main(String[] args) {
        ProfessionFactory professionFactory = new ProfessionFactory();
        Profession doc = professionFactory.getProfession("Doctor");
        doc.print();
    }
}
Enter fullscreen mode Exit fullscreen mode

Abstract Factory Method Design Pattern :

Definition - Provides an interface for creating families of related or dependent objects without specifying their concrete classes. It allows you to create sets of related objects with a consistent interface.

Why/When to use -

  • Creating Object Families: Use the Abstract Factory pattern when you need to create families of related or dependent objects while ensuring that they work together cohesively.
  • Consistency Across Objects: When you want to ensure that the objects created by the factory have a consistent interface or adhere to a common theme, the Abstract Factory is ideal.
  • Adapting to Multiple Implementations: If you anticipate needing to switch between different implementations of related objects (e.g., different UI components), the Abstract Factory supports this flexibility.

Benefits

  • Family of Objects: Creates families of related or dependent objects while ensuring consistency across them.
  • Encapsulation: Encapsulates the details of object creation, providing an abstract interface for creating objects.
  • Configurable: Allows easy switching between different sets of related objects by changing the concrete factory.

UML -
Abstract Factory Method UML

Code -

import java.lang.RuntimeException;

interface Car{
    void startCar();
}

class Tata implements Car{
    public void startCar(){
        System.out.println("Tata Car Started");
    }
}

class Maruti implements Car{
    public void startCar(){
        System.out.println("Maruti Car Started");
    }
}

interface Bike{
    void startBike();
}

class Hero implements Bike{
    public void startBike(){
        System.out.println("Tata Car Started");
    }
}

class Honda implements Bike{
    public void startBike(){
        System.out.println("Maruti Car Started");
    }
}

interface VehicleFactory{
    Car getCar(String carType);
    Bike getBike(String bikeType);
}

class CarFactory implements VehicleFactory{
    public Car getCar(String carType){
        if(carType.equalsIgnoreCase("Tata")){
            return new Tata();
        }else if(carType.equalsIgnoreCase("Maruti")){
            return new Maruti();
        }else{
            throw new RuntimeException(carType + " Car does not exist");
        }
    }
    public Bike getBike(String bikeType){
        throw new RuntimeException(bikeType + " is not manufactured in CarFactory");
    }
}

class BikeFactory implements VehicleFactory{
    public Car getCar(String carType){
        throw new RuntimeException(carType + " is not manufactured in BikeFactory");
    }
    public Bike getBike(String bikeType){
        if(bikeType.equalsIgnoreCase("Hero")){
            return new Hero();
        }else if(bikeType.equalsIgnoreCase("Honda")){
            return new Honda();
        }else{
            throw new RuntimeException(bikeType + " Bike does not exist");
        }
    }
}

class AbstractFactoryCreator{
    public VehicleFactory getFactory(String factoryType){
        if(factoryType.equalsIgnoreCase("Car")){
            return new CarFactory();
        }else if(factoryType.equalsIgnoreCase("Bike")){
            return new BikeFactory();
        }else{
            throw new RuntimeException(factoryType + " Factory does not exist");
        }
    }
}

public class Client{
    public static void main (String[] args) {
        AbstractFactoryCreator abstractFactoryCreator = new AbstractFactoryCreator();
        VehicleFactory vehicleFactory = abstractFactoryCreator.getFactory("car");

        Car myCar = vehicleFactory.getCar("tata");
        myCar.startCar();
    }
}
Enter fullscreen mode Exit fullscreen mode

In conclusion, Creational Design Patterns provide essential tools for efficient, maintainable, and adaptable software. Choose patterns wisely to create robust solutions. Happy coding!
I hope you found my blog enjoyable and informative.

Top comments (0)