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:
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
There are two main ways to implement the Builder Pattern:
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:
When Not to Use
Advantages of the Builder Pattern
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
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
领英推荐
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
Implementation Approaches
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
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
Structure
Implementation Steps
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
Drawbacks
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.