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.
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;
}
}
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
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 +
'}';
}
}
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();
}
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;
}
}
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());
}
}
The expected output of this code would be:
First House: House{walls=4, windows=8, doors=2, wallMaterial='Brick', woodType='Oak', hasPool=true}
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
1.Create the Product interface:
an interface that define a contract for all Product subclasses objects
public interface PaymentGateway {
void transfer();
void validate();
}
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");
}
}
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");
}
}
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();
}
}
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
1.Define Abstract Product Interfaces
public interface Button {
void render();
}
public interface Checkbox {
void render();
}
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");
}
}
3.Define the Abstract Factory Interface
public interface UiFactory {
Button createButton();
Checkbox createCheckbox();
}
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();
}
}
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);
}
}
}
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();
}
}
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
1.Define the Prototype interface with a clone() method
public interface Prototype<T> {
T clone();
}
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
}
3.Usage
public class Main {
public static void main(String[] args) {
User originalUser = new User("John", "Doe");
originalUser.addRole("USER");
User clonedUser = originalUser.clone();
}
}
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
// 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;
}
}
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.
Top comments (0)