Design Patterns - Creational

Design Patterns - Creational

Creational Design Pattern

Creational design patterns are a category of design patterns in software engineering that deal with object creation mechanisms. These patterns aim to abstract the instantiation process, making a system independent of how its objects are created, composed, and represented.

Creational design patterns offer various ways to manage the object creation process, ensuring that the system remains decoupled from the specifics of object construction. Here, we explore several prominent creational design patterns:

  1. Builder Pattern
  2. Factory Method Pattern
  3. Abstract Factory Pattern
  4. Singleton Pattern
  5. Object Pool Pattern

Builder Pattern

The Builder Pattern is a creational design pattern that facilitates the construction of complex objects by separating the construction process from the representation. This pattern is particularly useful when an object needs to be created with many optional parameters or when the creation process involves several steps. Unlike other creational patterns that construct objects in a single step, the Builder pattern focuses on constructing a complex object through a series of steps (or method calls) that specify the parts and configuration of the object.

Key Concepts

  1. Product: The complex object that is being built.
  2. Builder: An abstract interface that defines the steps required to build the product.
  3. Concrete Builder: A class that implements the Builder interface, providing specific implementations for the construction steps.
  4. Director: An optional class that directs the building process, ensuring that the steps are executed in a particular sequence.

There are two main ways to implement the Builder Pattern:

  1. Static Inner Builder Class: The builder class is implemented as a static inner class within the class it builds.
  2. Separate Builder Class: The builder class is implemented as a separate class

Implementing the Builder Pattern in Java - Inner Builder Class

Let's consider a Car class with various attributes such as make, model, year, and color. Using the Builder Pattern, we can simplify the construction of Car objects with different combinations of these attributes.

public class Car {
    private final String make;
    private final String model;
    private final int year;
    private final String color;

    private Car(Builder builder) {
        this.make = builder.make;
        this.model = builder.model;
        this.year = builder.year;
        this.color = builder.color;
    }

    public static class Builder {
        private String make;
        private String model;
        private int year;
        private String color;

        public Builder make(String make) {
            this.make = make;
            return this;
        }

        public Builder model(String model) {
            this.model = model;
            return this;
        }

        public Builder year(int year) {
            this.year = year;
            return this;
        }

        public Builder color(String color) {
            this.color = color;
            return this;
        }

        public Car build() {
            return new Car(this);
        }
    }

    @Override
    public String toString() {
        return "Car{" +
                "make='" + make + '\'' +
                ", model='" + model + '\'' +
                ", year=" + year +
                ", color='" + color + '\'' +
                '}';
    }

    public static void main(String[] args) {
        Car car = new Car.Builder()
                .make("Tesla")
                .model("Model S")
                .year(2023)
                .color("Red")
                .build();

        System.out.println(car);
    }
}        

Lombok is a Java library that reduces boilerplate code by generating code at compile-time using annotations. The @Builder annotation in Lombok makes it incredibly easy to implement the Builder Pattern. When the @Builder annotation is applied, Lombok generates a static inner builder class with methods to set each attribute and a build method to construct the object.

The whole code simplifies to

import lombok.Builder;
import lombok.ToString;

@Builder
@ToString
public class Car {
    private final String make;
    private final String model;
    private final int year;
    private final String color;

    public static void main(String[] args) {
        Car car = Car.builder()
                .make("Tesla")
                .model("Model S")
                .year(2023)
                .color("Red")
                .build();

        System.out.println(car);
    }
}        

When to Use:

  • Complex Objects: When an object has many attributes, especially optional ones, the Builder Pattern provides a clear and flexible way to construct the object.
  • Immutable Objects: Builders facilitate the creation of immutable objects by ensuring all required attributes are set before construction.
  • Readable Code: Builders improve code readability by providing a fluent API for setting attributes.

When Not to Use

  • Simple Objects: For objects with a few attributes, using constructors or setters may be more straightforward.

Advantages of the Builder Pattern

  • Encapsulation: The complex construction logic is encapsulated within the builder, separate from the product’s representation.
  • Flexibility: Different builders can produce different representations of the product, providing flexibility in the construction process.
  • Reusability: The same construction process can create different products.


Factory Method Pattern

The Factory Method pattern defines an interface for creating an object but allows subclasses to alter the type of objects that will be created. It promotes loose coupling by eliminating the need to bind application-specific classes into the code.

public abstract class Creator {
    public abstract Product factoryMethod();
}

public class ConcreteCreator extends Creator {
    @Override
    public Product factoryMethod() {
        return new ConcreteProduct();
    }
}


public interface Product {
    void use();
}

public class ConcreteProduct implements Product {
    @Override
    public void use() {
        System.out.println("Using ConcreteProduct");
    }
}



public class Client {
    public static void main(String[] args) {
        Creator creator = new ConcreteCreator();
        Product product = creator.factoryMethod();
        product.use();
    }
}        

Abstract Factory Pattern

The Abstract Factory pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes. This pattern is particularly useful when a system needs to be independent of how its objects are created and represented, allowing for flexibility and scalability in creating various product families.

Structure

  1. Abstract Factory: Declares a set of methods for creating abstract product objects.
  2. Concrete Factory: Implements the operations to create concrete product objects.
  3. Abstract Product: Declares an interface for a type of product object.
  4. Concrete Product: Implements the Abstract Product interface.
  5. Client: Uses only interfaces declared by Abstract Factory and Abstract Product classes.

Java code Example

Let's consider a example of GUI framework that supports multiple themes. This example will showcase how you can use the Abstract Factory pattern to create a family of related objects, such as buttons and checkboxes, with different styles.

GUI Framework Example

Abstract Factory: GUIFactory

This interface declares the methods for creating abstract product objects (buttons and checkboxes).

// Abstract Factory
public interface GUIFactory {
    Button createButton();
    Checkbox createCheckbox();
}        

Abstract Product: Button and Checkbox

These interfaces declare the methods that the concrete products must implement.

// Abstract Product
public interface Button {
    void paint();
}

// Abstract Product
public interface Checkbox {
    void paint();
}        

Concrete Products: WinButton, WinCheckbox, MacButton, MacCheckbox

These classes implement the abstract product interfaces with specific behaviors for Windows and Mac themes.

// Concrete Product
public class WinButton implements Button {
    @Override
    public void paint() {
        System.out.println("Rendering a button in a Windows style.");
    }
}

// Concrete Product
public class WinCheckbox implements Checkbox {
    @Override
    public void paint() {
        System.out.println("Rendering a checkbox in a Windows style.");
    }
}

// Concrete Product
public class MacButton implements Button {
    @Override
    public void paint() {
        System.out.println("Rendering a button in a Mac style.");
    }
}

// Concrete Product
public class MacCheckbox implements Checkbox {
    @Override
    public void paint() {
        System.out.println("Rendering a checkbox in a Mac style.");
    }
}        

Concrete Factories: WinFactory and MacFactory

These classes implement the abstract factory interface and return the appropriate concrete products for Windows and Mac themes.

// Concrete Factory
public class WinFactory implements GUIFactory {
    @Override
    public Button createButton() {
        return new WinButton();
    }

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

// Concrete Factory
public class MacFactory implements GUIFactory {
    @Override
    public Button createButton() {
        return new MacButton();
    }

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

Client

The client uses the abstract factory and abstract product interfaces. It is independent of the concrete factory and product classes.

public class Application {
    private Button button;
    private Checkbox checkbox;

    public Application(GUIFactory factory) {
        button = factory.createButton();
        checkbox = factory.createCheckbox();
    }

    public void paint() {
        button.paint();
        checkbox.paint();
    }

    public static void main(String[] args) {
        // Use a Windows factory
        GUIFactory factory = new WinFactory();
        Application app = new Application(factory);
        app.paint();

        // Use a Mac factory
        factory = new MacFactory();
        app = new Application(factory);
        app.paint();
    }
}        

The Abstract Factory pattern allows you to create families of related objects without specifying their concrete classes. This example demonstrates how to use the pattern to create a GUI framework that supports multiple themes (Windows and Mac). The client code is independent of the specific factories and products, allowing for flexibility and scalability in adding new themes or product families.

Benefits

  1. Isolation of concrete classes: The client code works with interfaces provided by the abstract factory and abstract product classes, keeping the client independent of the concrete classes.
  2. Ease of swapping families of products: New families of products can be introduced without altering the client code.
  3. Consistency among products: The abstract factory ensures that products created are compatible with each other.

The Abstract Factory pattern provides a way to encapsulate a group of individual factories that have a common theme. It helps in making the client independent of how objects are created, composed, and represented, allowing for flexibility and consistency in creating families of related objects.


Singleton Pattern

The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. This is particularly useful when exactly one object is needed to coordinate actions across the system. Common use cases include configuration objects, logging, caching, thread pools, etc.

Key Points

  • Single Instance: Only one instance of the class is created.
  • Global Access: The single instance is globally accessible.
  • Lazy Initialization: The instance is created only when it is needed.

Implementation Approaches

  1. Eager Initialization

This method creates the instance at the time of class loading.

public class EagerSingleton {
    private static final EagerSingleton instance = new EagerSingleton();

    private EagerSingleton() {}

    public static EagerSingleton getInstance() {
        return instance;
    }
}        

Lazy Initialization

This method creates the instance only when it is requested for the first time.

public class LazySingleton {
    private static LazySingleton instance;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}        

Thread-Safe Singleton

This method ensures that the instance is created in a thread-safe manner.

public class ThreadSafeSingleton {
    private static ThreadSafeSingleton instance;

    private ThreadSafeSingleton() {}

    public static synchronized ThreadSafeSingleton getInstance() {
        if (instance == null) {
            instance = new ThreadSafeSingleton();
        }
        return instance;
    }
}        

Bill Pugh Singleton Design

This method uses a static inner helper class to create the Singleton instance. It is both lazy and thread-safe.

public class BillPughSingleton {
    private BillPughSingleton() {}

    private static class SingletonHelper {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}        

Enum Singleton

This method leverages the enum feature to create the Singleton instance. It is thread-safe and protects against serialization.

public enum EnumSingleton {
    INSTANCE;

    public void doSomething() {
        // Logic
    }
}        

Benefits of Singleton Pattern

  1. Controlled Access to the Single Instance: Singleton ensures that a class has only one instance and provides a global point of access to it.
  2. Flexibility: It allows a variable number of instances in specific circumstances.

The Singleton pattern is a powerful design pattern that ensures a class has only one instance and provides a global point of access to it. By controlling the instance creation and providing a single point of access, Singleton helps in managing shared resources effectively. However, it should be used judiciously to avoid global state issues and hidden dependencies. The example from Apache Log4j demonstrates its practical application in a widely used logging framework, ensuring a single instance of the logging configuration manager.


Object Pool Pattern

The Object Pool pattern is a creational design pattern that manages a pool of reusable objects. When a new object is needed, the pool is queried for an available object instead of creating a new one. If an object is available, it is returned; otherwise, a new object is created and added to the pool. This pattern is particularly useful for managing resources like database connections, sockets, and threads, where the cost of creating and destroying objects is high.

Key Points

  1. Reuse of Objects: Objects are reused instead of being created and destroyed frequently.
  2. Performance Improvement: Reduces the overhead of object creation and garbage collection.
  3. Resource Management: Efficiently manages a limited number of expensive resources.

Structure

  1. Client: The object that uses the objects from the pool.
  2. Object Pool: Manages the pool of reusable objects.
  3. Reusable Object: The objects that are being managed and reused by the pool.

Implementation Steps

  1. Create an interface for reusable objects.
  2. Implement the pool class.
  3. Implement the reusable object class.
  4. Client code to use the object pool.

Java code Example: Database Connection Pool

We'll create a simplified version of a database connection pool to illustrate the Object Pool pattern.

1. Reusable interface

public interface Connection {
    void connect();
    void disconnect();
}        

2. Concrete Reusable Object: DatabaseConnection

public class DatabaseConnection implements Connection {
    private String connectionString;

    public DatabaseConnection(String connectionString) {
        this.connectionString = connectionString;
    }

    @Override
    public void connect() {
        System.out.println("Connecting to database: " + connectionString);
    }

    @Override
    public void disconnect() {
        System.out.println("Disconnecting from database: " + connectionString);
    }
}        

3. Object Pool: ConnectionPool

public class ConnectionPool {
    private Queue<Connection> availableConnections = new LinkedList<>();
    private Queue<Connection> inUseConnections = new LinkedList<>();
    private String connectionString;
    private int maxPoolSize;

    public ConnectionPool(String connectionString, int maxPoolSize) {
        this.connectionString = connectionString;
        this.maxPoolSize = maxPoolSize;
    }

    public synchronized Connection getConnection() {
        if (availableConnections.isEmpty() && inUseConnections.size() < maxPoolSize) {
            availableConnections.add(new DatabaseConnection(connectionString));
        }

        Connection connection = availableConnections.poll();
        if (connection != null) {
            inUseConnections.add(connection);
        }
        return connection;
    }

    public synchronized void releaseConnection(Connection connection) {
        inUseConnections.remove(connection);
        availableConnections.add(connection);
    }
}        

4. Client code

public class Client {
    public static void main(String[] args) {
        ConnectionPool pool = new ConnectionPool("jdbc:mysql://localhost:3306/mydb", 5);

        Connection connection1 = pool.getConnection();
        connection1.connect();
        
        Connection connection2 = pool.getConnection();
        connection2.connect();

        pool.releaseConnection(connection1);
        pool.releaseConnection(connection2);
    }
}        

Benefits

  1. Resource Reuse: Objects are reused, reducing the overhead of creating and destroying objects.
  2. Performance Improvement: Efficiently manages expensive resources.
  3. Scalability: Manages a limited number of resources, allowing the system to scale better.

Drawbacks

  1. Complexity: Adds complexity to the system due to the management of the pool.
  2. Potential Resource Leaks: Improper management can lead to resource leaks.

The Object Pool pattern is a powerful creational design pattern that manages a pool of reusable objects, improving performance and resource management. The example provided demonstrates how to implement a simple connection pool in Java. This pattern is especially useful for managing expensive resources like database connections, threads, and network sockets.

Blog is also published on Substack

In the next article, we will explore Structural Design Patterns. These patterns focus on how classes and objects are composed to form larger structures, ensuring that these structures are both flexible and efficient. Stay tuned to learn how structural patterns can help in creating well-organized and scalable software architectures.

要查看或添加评论,请登录

Kiran U Kamath的更多文章

社区洞察

其他会员也浏览了