DEV Community

Cover image for Design patterns - Creational Patterns
Mohammed Mazene ZERGUINE
Mohammed Mazene ZERGUINE

Posted on

Design patterns - Creational Patterns

In software engineering, a design pattern is a well-proven solution to a recurring problem in software design.
These patterns provide a structured approach to solving common architectural and design challenges, ensuring reusability, maintainability, and scalability. T
They provide standardized solutions that developers can reuse across various projects.

Why Use Design Patterns ?

Software development often involves solving complex problems. Without proper structuring, applications can become difficult to manage, extend, and scale. Design patterns help mitigate these issues by:

  • Providing reusable and proven solutions
  • Enhancing maintainability and reducing code duplication
  • Improving scalability and flexibility
  • Organizing code in a structured and readable manner
  • Encapsulating best practices

When to Consider Using a Design Pattern ?

You should consider applying a design pattern when:

  • You encounter a problem that a pattern is specifically designed to solve.
  • Scalability and maintainability are major concerns.
  • You are working on large applications or within a large development team where consistency is crucial.

Categories of Design Patterns

Design patterns are classified into three main categories:

šŸ”¹ Creational Patterns

Focus on object creation mechanisms, making instantiation more flexible and reusable.

šŸ”¹ Structural Patterns

Define how objects and classes interact to form larger structures.

šŸ”¹ Behavioral Patterns

Manage object interactions and responsibilities, improving communication between components.

Design patterns categories

Exploring Creational Design Patterns

In this article, we will explore Creational Design Patterns which focus on efficient and flexible object creation strategies

These patterns abstract complex instantiation processes, making the system more adaptable and easier to maintain.

šŸ”¹ Builder
šŸ”¹ Factory
šŸ”¹ Abstract Factory
šŸ”¹ Prototype
šŸ”¹ Singleton

Builder Pattern

The builder pattern is a design pattern that allows the creation of complex objects step by step while allowing immutability the construction process can change based on the type of the product being created

This method allows same construction process to create different representations by separating the construction process from the presentation of the object

Problem:
In OOP managing complex objects can be challenging due to many reason

  • The object has many optional parameters, leading to unreadable and error-prone constructor calls
  • constructor overloading with too many variations.
public class House {
    // If new parameters are added, all constructors must be updated //
    private  int walls;
    private  int windows;
    private int doors;
    private String WallMaterial;
    private String woodType;
    private int hasPool;



    // To many constructors (Hard to maintain) //
    public House() {

    }
    // To many parameters hard to read // 
    public House(int walls, int windows, int doors, String wallMaterial, String woodType, int hasPool) {
        this.walls = walls;
        this.windows = windows;
        this.doors = doors;
        WallMaterial = wallMaterial;
        this.woodType = woodType;
        this.hasPool = hasPool;
    }

    public House(int walls, int windows, int doors) {
        this.walls = walls;
        this.windows = windows;
        this.doors = doors;
    }
}


Enter fullscreen mode Exit fullscreen mode

Solution:
The builder pattern solves this problem by
āœ… Encapsulating creation logic in a seperate class
āœ… Using Method chaining for readability
āœ… Ensuring that the final product immutable

Diagramme

implementation:

1.Define the Class (Product):

The Product class represents the object to be built. It contains multiple fields that define its properties. These fields are declared final, ensuring immutability. A private constructor enforces object creation through a builder.

public class House {
    private final int walls;
    private final int windows;
    private final int doors;
    private final String wallMaterial;
    private final String woodType;
    private final boolean hasPool;

    // Private constructor to enforce object creation through the Builder

   House(int walls, int windows, int doors, String wallMaterial, String woodType, boolean hasPool) {
        this.walls = walls;
        this.windows = windows;
        this.doors = doors;
        this.wallMaterial = wallMaterial;
        this.woodType = woodType;
        this.hasPool = hasPool;
    }

    // Getter methods for accessing private fields
    public int getWalls() { return walls; }
    public int getWindows() { return windows; }
    public int getDoors() { return doors; }
    public String getWallMaterial() { return wallMaterial; }
    public String getWoodType() { return woodType; }
    public boolean hasPool() { return hasPool; }

    @Override
    public String toString() {
        return "House{" +
                "walls=" + walls +
                ", windows=" + windows +
                ", doors=" + doors +
                ", wallMaterial='" + wallMaterial + '\'' +
                ", woodType='" + woodType + '\'' +
                ", hasPool=" + hasPool +
                '}';
    }
}

Enter fullscreen mode Exit fullscreen mode

2.Define the Builder Interface:

The Builder interface defines the steps required to build a House object. It enforces consistency among different builders and ensures that each builder follows the same construction process.

public interface Builder {
    House build();
    void reset(); 
}
Enter fullscreen mode Exit fullscreen mode

3.Create the Concrete Builder Class:

The Concrete Builder class provides a step-by-step approach for constructing a House. It allows method chaining for a readable and elegant object creation process.

public class HouseBuilder implements Builder {
    private int walls;
    private int windows;
    private int doors;
    private String wallMaterial;
    private String woodType;
    private boolean hasPool;

    public HouseBuilder buildWalls(int walls) {
        this.walls = walls;
        return this;
    }

    public HouseBuilder buildWindows(int windows) {
        this.windows = windows;
        return this;
    }

    public HouseBuilder buildDoors(int doors) {
        this.doors = doors;
        return this;
    }

    public HouseBuilder buildWallMaterial(String material) {
        this.wallMaterial = material;
        return this;
    }

    public HouseBuilder buildWoodType(String woodType) {
        this.woodType = woodType;
        return this;
    }

    public HouseBuilder buildPool(boolean hasPool) {
        this.hasPool = hasPool;
        return this;
    }

    @Override
    public House build() {
        return new House(this.walls, this.windows, this.doors, this.wallMaterial, this.woodType, this.hasPool);
    }

    @Override
    public void reset() {
        this.walls = 0;
        this.windows = 0;
        this.doors = 0;
        this.wallMaterial = null;
        this.woodType = null;
        this.hasPool = false;
    }
}

Enter fullscreen mode Exit fullscreen mode

4.Usage:

public class Main {
    public static void main(String[] args) {
        House house = new HouseBuilder()
                .buildWalls(4)
                .buildWindows(8)
                .buildDoors(2)
                .buildWallMaterial("Brick")
                .buildWoodType("Oak")
                .buildPool(true)
                .build();

        System.out.println("First House: " + house.toString());
    }
}

Enter fullscreen mode Exit fullscreen mode

The expected output of this code would be:

First House: House{walls=4, windows=8, doors=2, wallMaterial='Brick', woodType='Oak', hasPool=true}

Enter fullscreen mode Exit fullscreen mode

Advantages:
āœ… Immutable Objects
āœ… Step-by-Step Construction
āœ… Improved Readability and code Maintainability
āœ… Reduces Constructor Overloading

Limitations:
āŒ More Code & Complexity
āŒ Overhead for Simple Objects
āŒ Increased Memory Usage
āŒ Can Lead to Code Duplication

Conclusion:
The Builder Pattern is a powerful and flexible approach for creating complex objects. However, it adds complexity and is not always necessary for simple objects


Factory Pattern

The Factory Pattern provides an interface for creating objects without specifying their concrete classes allowing subclasses or implementing classes to determine which class to instantiate. The pattern delegates the responsibility of object creation to subclasses which implement the factory method to produce objects

  • It creates objects without exposing the instantiation logic
  • It refers to the created objects through a common interface
  • It decouples the client code from the classes being instantiated

Problem:

  • Tight coupling between business code and concrete classes making the systeme hard to modify
  • Violation of the Open/Closed Principle
  • Cases of complex object creation logic
  • Conditional instantiation complexity

Solution:
āœ… Reducing the high coupling by having the business code rely on the factory interface instead of the concrete classes
āœ… New product types can be created by adding new factory methods wich alling with the open/close principale (code should be open to extension and closed to modification)
āœ… Encapsulate complex creation logic (validations, configurations ..Etc) in the factory methods and keeps the business code clean and only focused on using the objects
āœ… Complex conditional creations are hidden in the factory implementation removing complex conditions from the business code and keeping it clean

Factory diagram

1.Create the Product interface:
an interface that define a contract for all Product subclasses objects

public interface PaymentGateway {
    void transfer();
    void validate();
}
Enter fullscreen mode Exit fullscreen mode

2.Create the Product subclasses implementations

public class StripeGateway implements PaymentGateway {
    @Override
    public void transfer() {
        System.out.println("Stripe gateway transfer");
    }

    @Override
    public void validate() {
        System.out.println("Stripe gateway validation");
    }
}

public class PaypalGateway implements PaymentGateway {
    @Override
    public void transfer() {
        System.out.println("Paypal gateway transfer");
    }

    @Override
    public void validate() {
        System.out.println("Paypal gateway validation");
    }
}
Enter fullscreen mode Exit fullscreen mode

3.Create a factory class with static method create to return dynamicly the created objects

public enum PaymentType {
    STRIPE, PAYPAL
}


public class PaymentGatewayFactory {
    private static final Map<PaymentType, Supplier<PaymentGateway>> gatewayMap = new HashMap<>();

    // Registering available payment gateways
    static {
        gatewayMap.put(PaymentType.STRIPE, StripeGateway::new);
        gatewayMap.put(PaymentType.PAYPAL, PaypalGateway::new);
    }

    // Factory method to create instances
    public static PaymentGateway create(PaymentType type) {
        Supplier<PaymentGateway> supplier = gatewayMap.get(type);
        if (supplier != null) {
            return supplier.get();
        }
        throw new IllegalArgumentException(type + " gateway is not supported");
    }
}

Enter fullscreen mode Exit fullscreen mode

4.Call the create method in business code to create instance of the desierd product

public class Main {
    public static void main(String[] args) {
        // Create Stripe payment gateway
        PaymentGateway stripe = PaymentGatewayFactory.create(PaymentType.STRIPE);
        stripe.validate();
        stripe.transfer();

        // Create Paypal payment gateway
        PaymentGateway paypal = PaymentGatewayFactory.create(PaymentType.PAYPAL);
        paypal.validate();
        paypal.transfer();
    }
}
Enter fullscreen mode Exit fullscreen mode

Advantges:
āœ… Encapsulation of Object Creation
āœ… Loose Coupling
āœ… Maintenance and Scalability
āœ… Code Reusability
āœ… Open-Closed Principle (OCP)

Limitations:
āŒ Increases Complexity
āŒ Can Violate Single Responsibility Principle

Conclusion:
The Factory Pattern simplifies object creation, promotes loose coupling, and enhances maintainability. However, it can add unnecessary complexity if overused. It's best suited for scenarios where object instantiation logic is complex or frequently changing


Abstract Factory Pattern

The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes. It encapsulates a group of individual factories that have a common theme, allowing client code to create objects from multiple related families without knowing their specific implementations

Problem:

  • Lack of Support for Families of Related Objects
  • risk of mixing incompatible objects
  • If the client needs multiple related objects we must create them separately leading to duplicated instantiation logic

Solution:
āœ… Encapsulates Object Families by providing a single interface for creating multiple related objects

Abstract Factory diagramme

1.Define Abstract Product Interfaces

public interface Button {
    void render();
}

public interface Checkbox {
    void render();
}

Enter fullscreen mode Exit fullscreen mode

2.Create the Concrete Product Implementations

public class DarkButton implements Button {
    @Override
    public void render() {
        System.out.println("Rendering Dark Theme Button");
    }
}

public class LightButton implements Button {
    @Override
    public void render() {
        System.out.println("Rendering Light Theme Button");
    }
}

public class DarkCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("Rendering Dark Theme Checkbox");
    }
}

public class LightCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("Rendering Light Theme Checkbox");
    }
}

Enter fullscreen mode Exit fullscreen mode

3.Define the Abstract Factory Interface

public interface UiFactory {
    Button createButton();
    Checkbox createCheckbox();
}
Enter fullscreen mode Exit fullscreen mode

4.Define the concrete factories implementations

public class DarkThemeFactory implements UiFactory {
    // Singleton instance <Check singleton pattern>
    private static final DarkThemeFactory INSTANCE = new DarkThemeFactory();
    private DarkThemeFactory() {} 

    public static DarkThemeFactory getInstance() {
        return INSTANCE;
    }

    @Override
    public Button createButton() {
        return new DarkButton();
    }

    @Override
    public Checkbox createCheckbox() {
        return new DarkCheckbox();
    }
}

public class LightThemeFactory implements UiFactory {
    private static final LightThemeFactory INSTANCE = new LightThemeFactory(); 

    private LightThemeFactory() {}

    public static LightThemeFactory getInstance() {
        return INSTANCE;
    }

    @Override
    public Button createButton() {
        return new LightButton();
    }

    @Override
    public Checkbox createCheckbox() {
        return new LightCheckbox();
    }
}

Enter fullscreen mode Exit fullscreen mode

5.Define the factory provider


public enum ThemeType { 
DARK, LIGHT
}

public class FactoryProvider {
    public static UiFactory getFactory(ThemeType theme) {
        switch (theme) {
            case DARK:
                return DarkThemeFactory.getInstance();
            case LIGHT:
                return LightThemeFactory.getInstance();
            default:
                throw new IllegalArgumentException("Unknown theme: " + theme);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

6.Usage

public class Main {
    public static void main(String[] args) {
        UiFactory darkFactory = FactoryProvider.getFactory(ThemeType.DARK);
        Button darkButton = darkFactory.createButton();
        Checkbox darkCheckbox = darkFactory.createCheckbox();

        darkButton.render();
        darkCheckbox.render();

        UiFactory lightFactory = FactoryProvider.getFactory(ThemeType.LIGHT);
        Button lightButton = lightFactory.createButton();
        Checkbox lightCheckbox = lightFactory.createCheckbox();

        lightButton.render();
        lightCheckbox.render();
    }
}

Enter fullscreen mode Exit fullscreen mode

Advantages:
āœ… Consistency Across Related Objects
āœ… Loose Coupling
āœ… Maintainability

Limitations:
āŒ Increased Complexity
āŒ Difficult to Extend

Conclusion:
The Abstract Factory Pattern ensures consistency across families of related objects while promoting loose coupling and maintainability. However, it adds complexity and makes adding new product types harder. It's best used when a system requires multiple related objects that must work together


Prototype Pattern

The prototype pattern or also known as Clone pattern allows to object duplication by cloning an existing object instead of creating a new instance from scratch. This pattern is useful when object creation is costly or complex, and cloning provides a more efficient alternative

Problem:

  • Expensive Object Creation
  • Complex Object Configuration
  • Too many constructors or factory methods make maintenance difficult

Solution:
āœ… Clone an existing object instead of recreating it
āœ… Cloning allows flexible object variations without subclassing

Clone Pattern

1.Define the Prototype interface with a clone() method

public interface Prototype<T> {
    T clone();
}

Enter fullscreen mode Exit fullscreen mode

2.Define the Product interface that implements the Prototype with a creation constructor, copy constructor and the clone method implementation

public class User implements Prototype<User> {
    private String name;
    private String lastName;
    private List<String> roles;

public User(String name, String lastName) {
        this.name = name;
        this.lastName = lastName;
        this.roles = new ArrayList<>();
    }


   // Copy constructor 
    public User(User source) {
        this.name = source.name;
        this.lastName = source.lastName;
        this.roles = new ArrayList<>(source.roles);
    }

    @Override
    public User clone() {
        return new User(this);
    }

    // Getters and setters
}
Enter fullscreen mode Exit fullscreen mode

3.Usage

public class Main {
    public static void main(String[] args) {
        User originalUser = new User("John", "Doe");
        originalUser.addRole("USER");

        User clonedUser = originalUser.clone();
    }
}

Enter fullscreen mode Exit fullscreen mode

Advantages:
āœ… Reduces Subclassing & Constructor Complexity
āœ… Improves Maintainability

Limitations:
āŒ Deep Cloning Complexity

Conclusion:
The Prototype Pattern simplifies object creation by cloning existing instances, making it efficient for complex or expensive object initialization but requires careful handling of deep copies

Use it when object duplication is frequent and performance optimization is needed


Singleton Pattern

The Singleton Pattern is a ensures a class has only one instance accros the whole application and provides a global access point to that instance It restricts instantiation to a single object, making it ideal for managing shared resources like configurations, logging, and database connections ..etc

Problems:

  • the need of global accessibility without using global variables
  • Conflicting Instantiations
  • inconsistent states due to multiple instances and unnecessary resource consumption

solution
āœ… Ensures only one instance is created across threads

Singleton description

// synchronized for thread safety 
public class SingletonInstance {
    private static volatile SingletonInstance instance;

    private SingletonInstance() {}

    public static SingletonInstance getInstance() {
        if (instance == null) {
            synchronized (SingletonInstance.class) {
                if (instance == null) {
                    instance = new SingletonInstance();
                }
            }
        }
        return instance;
    }
}


Enter fullscreen mode Exit fullscreen mode

Advatages
āœ… Global Access Point
āœ… Lazy Initialization
āœ… Thread Safety
āœ… Reduces Memory Waste

Limitations
āŒ Breaks the Single Responsibility Principle by being responsible for both creating and managing its own instance
āŒ Hinders Unit Testing

Conclusion
The Singleton Pattern ensures a single instance of a class, useful for shared resources like logging and databases

Comparison of Creational Design Patterns

Pattern Purpose Key Benefits Common Use Cases
Builder Constructs complex objects step by step Readability, Immutability, Flexibility UI Builders, Query Builders, API Clients
Factory Encapsulates object creation logic Loose Coupling, Scalability Dependency Injection, Payment Processing
Abstract Factory Creates families of related objects Consistency, Encapsulation UI Themes, Cross-Platform Development
Prototype Clones existing objects Performance Optimization, Simplifies Instantiation Game Development, Document Cloning
Singleton Ensures a single instance Global Access, Controlled Instantiation Logging, Database Connections

Conculision

Creational design patterns provide flexible, scalable, and maintainable ways to create objects in software development. Whether you're dealing with complex object creation (Builder Pattern), ensuring a single instance (Singleton Pattern), or managing families of related objects (Abstract Factory), these patterns help structure your code effectively.

By understanding these patterns, you can improve your software architecture and write cleaner, more reusable code.

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

šŸ‘‹ Kindness is contagious

Please leave a ā¤ļø or a friendly comment on this post if you found it helpful!

Okay