tech-notes-and-questions

Creational Design Patterns

[definition, purpose, use cases, design, example, implementation]

Creational design patterns are concerned with the process of object creation. They provide various ways to create objects while hiding the complexities of the instantiation process.

  1. Singleton - ensures a class has only one instance and provides a global point of access to it
  2. Factory Method - defines an interface for creating objects, but lets subclasses decide which class to instantiate
  3. Abstract Factory - provides an interface for creating families of related or dependent objects without specifying their concrete classes
  4. Builder - separates the construction of a complex object from its representation, allowing the same construction process to create different objects
  5. Prototype - creates new objects by copying an existing object, known as a prototype

1. Singleton Design Pattern

Definition

A design pattern that ensures a class has only one instance and provides a global point of access to that instance.

Purpose

This is useful when you want to control access to a shared resource, like a configuration object or a connection pool.

Use cases

1. Thread Pool Management

2. Caching

3. Database Connections

4. Logging

Design

Example

Let’s implement a simple Java class that behaves like a Singleton class

Implementation

  1. Create the SingletonClass for which you want only one instance created (lazy initialization)
public class SingletonClass {
    
    public static SingletonClass instance;
    // (eager initialization)
    // public static SingletonClass instance = new SingletonClass();
    
    
    // blocking the constructor so it's not accessible for multiple instantiations
    private SingletonClass() {
    }
    
    /*
        'synchronized' so that even when multiple threads try to access, 
         only one thread can instantiate it & the others will use that instance
     */
    public static synchronized SingletonClass getInstance() {
        
        // (lazy initialization)
        // 'instance==null' check to ensure all the threads use the one instance that's already created
        if (instance==null) {
            instance = new SingletonClass();
        }
        return instance;
    }
}
  1. Client code demonstrate Singleton usage
public static void main (String[] args) {
    SingletonClass instance1 = SingletonClass.getInstance();
    SingletonClass instance2 = SingletonClass.getInstance();

    System.out.println(instance1 == instance2); // true, same instance
}


</br>

2. Factory Pattern

Definition

Benefits

Purpose

Used in various scenarios where object creation requires flexibility, decoupling, or the creation logic can change based on specific conditions without the client having to know which subclasses to create.

Use cases

1. To support multiple payment options in the future

2. When You Want to Hide Complex Object Construction Logic

3. When You Need to Manage the Versioning of Objects

Design

Example

Let’s say we have different vehicles like Bike, Car, etc. We want the client to initiate instances as per required without having to know about the associated concrete classes.

Implementation

  1. Create the Vehicle interface
interface Vehicle {
    void drive();
}
  1. Create the Concrete classes Car, Bike, etc.
public class Car implements Vehicle {
    
    @Override
    void drive() {
        System.out.println("Driving a Car...");
    }
}
public class Bike implements Vehicle {
    
    @Override
    void drive() {
        System.out.println("Driving a Bike...");
    }
}
  1. Create the Factory class with methods to decide the type of class for the object (object creation logic)
public class VehicleFactory {
    public Vehicle getVehicle(String vehicleName) {
        if(vehicleName.equalsIgnoreCase("car")) {
            return new Car();
        }
        else if(vehicleName.equalsIgnoreCase("bike")) {
            return new Bike();
        }
        
        return null;
    }
}
  1. Client code to demonstrate the usage
    public static void main (String[] args) {
     VehicleFactory vehicleFactory = new VehicleFactory();
        
     Vehicle vehicle1 = vehicleFactory.getVehicle("car");
     Vehicle vehicle2 = vehicleFactory.getVehicle("bike");
        
     vehicle1.drive(); // Driving a Car...
     vehicle2.drive(); // Driving a Bike...
    }
    


</br>

3. Abstract Factory Pattern

Definition

Purpose

It’s useful when you need to create objects from several related classes without knowing their exact types.

Use cases

Design

Example

Let’s take the same example as above - vehicles like Car, Bike but with subcategories of Luxury vehicles & Economic vehicles

Implementation

  1. Create the Vehicle interface
interface Vehicle {
    void move();
}
  1. Create the ConcreteProduct classes
public class EconomicCar implements Vehicle {

    @Override
    void move() {
        System.out.println("Driving an Economic Car...");
    }
}
public class LuxuryCar implements Vehicle {

    @Override
    void move() {
        System.out.println("Driving a Luxury Car...");
    }
}
public class EconomicBike implements Vehicle {

    @Override
    void move() {
        System.out.println("Riding an Economic Bike...");
    }
}
public class LuxuryBike implements Vehicle {

    @Override
    void move() {
        System.out.println("Riding a Luxury Bike...");
    }
}
  1. Create the abstract VehicleFactory interface
public interface VehicleFactory {
    Vehicle getVehicle(String vehicleName);
} 
  1. Create the ConcreteVehicleFactory classes that group the products
public class EconomicVehicleFactory implements VehicleFactory {
    
    @Override
    Vehicle getVehicle(String vehicleName) {
        if (vehicleName.equalsIgnoreCase("car")) {
            return new EconomicCar();
        }
        else if(vehicleName.equalsIgnoreCase("bike")) {
            return new EconomicBike();
        }
        
        return null;
    }
}
public class LuxuryVehicleFactory implements VehicleFactory {
    
    @Override
    Vehicle getVehicle(String vehicleName) {
        if (vehicleName.equalsIgnoreCase("car")) {
            return new LuxuryCar();
        }
        else if(vehicleName.equalsIgnoreCase("bike")) {
            return new LuxuryBike();
        }
        
        return null;
    }
}
  1. Client code to demonstrate the usage
public static void main (String[] args) {
    VehicleFactory economicFactory = new EconomicVehicleFactory();
    VehicleFactory luxuryFactory = new LuxuryVehicleFactory();
    
    Vehicle vehicle1 = economicFactory.getVehicle("car");   
    Vehicle vehicle2 = luxuryFactory.getVehicle("car");     
    Vehicle vehicle3 = economicFactory.getVehicle("bike");  
    Vehicle vehicle4 = luxuryFactory.getVehicle("bike");    
    
    vehicle1.move();  // Driving an Economy Car...
    vehicle2.move();  // Driving a Luxury Car...
    vehicle3.move();  // Riding an Economic Bike...
    vehicle4.move();  // Riding a Luxury Bike...
}


</br>

4. Builder Pattern

Definition

The pattern separates the construction of an object from its representation, allowing for more control over the object creation process.

Purpose

When an object needs to be created with many possible configurations or when the construction process involves multiple steps.

Use cases

1. Object Creation with Many Optional Parameters

Scenario: An object has several optional parameters that may or may not be used. Passing null or using multiple overloaded constructors can lead to unreadable code.

2. Immutable Objects with Configurable Parameters

Scenario: When you want to build an object with many configurable properties while ensuring immutability. The builder pattern helps ensure that once an object is constructed, it cannot be modified.

Use Case: Creating immutable classes like Person, Book, or Bank Account, where you want to avoid setters and ensure the object is completely initialized once constructed.

Example: An immutable Person object with mandatory parameters (name, age) and optional parameters (address, phone number, etc.).

3. Building Different Representations of the Same Object

Scenario: You may need to build different representations of the same object depending on the context (e.g., different variations or versions of the same object).

Use Case: When you need multiple representations of an object, such as building Report Generators, Document Parsers, or HTML/JSON/XML builders.

Example: A Document object that can be represented as HTML, PDF, or plain text, with common elements like title, body, and footer.

Design

Example

Imagine you want to build a house object, and it has multiple optional and mandatory fields. Instead of having a constructor with many parameters, you can use the builder pattern to construct the house step by step.

Implementation

  1. Create the House class with its fields & also a static class HouseBuilder inside it that builds the object of House type.
// Product class
public class House {
    // Required parameters
    private final String foundation;
    private final String structure;

    // Optional parameters
    private final boolean hasGarage;
    private final boolean hasSwimmingPool;
    private final boolean hasGarden;

    // Private constructor
    private House(HouseBuilder builder) {
        this.foundation = builder.foundation;
        this.structure = builder.structure;
        this.hasGarage = builder.hasGarage;
        this.hasSwimmingPool = builder.hasSwimmingPool;
        this.hasGarden = builder.hasGarden;
    }

    @Override
    public String toString() {
        return "House{" +
                "foundation='" + foundation + '\'' +
                ", structure='" + structure + '\'' +
                ", hasGarage=" + hasGarage +
                ", hasSwimmingPool=" + hasSwimmingPool +
                ", hasGarden=" + hasGarden +
                '}';
    }

    // Builder Class
    public static class HouseBuilder {
        // Required parameters
        private final String foundation;
        private final String structure;

        // Optional parameters - initialize with default values
        private boolean hasGarage = false;
        private boolean hasSwimmingPool = false;
        private boolean hasGarden = false;

        // Builder constructor with required parameters
        public HouseBuilder(String foundation, String structure) {
            this.foundation = foundation;
            this.structure = structure;
        }

        // Setters for optional parameters
        public HouseBuilder setGarage(boolean hasGarage) {
            this.hasGarage = hasGarage;
            return this; // Return builder to allow chaining
        }

        public HouseBuilder setSwimmingPool(boolean hasSwimmingPool) {
            this.hasSwimmingPool = hasSwimmingPool;
            return this;
        }

        public HouseBuilder setGarden(boolean hasGarden) {
            this.hasGarden = hasGarden;
            return this;
        }

        // Build method to create the House object
        public House build() {
            return new House(this);
        }
    }
}
  1. Client code to demonstrate the usage
public class BuilderPatternExample {
    public static void main(String[] args) {
        // Using the builder to create a complex House object
        House house = new House.HouseBuilder("Concrete", "Wood")
                .setGarage(true)
                .setSwimmingPool(false)
                .setGarden(true)
                .build();

        System.out.println(house);
    }
}


</br>

Explanation:


</br>

5. Prototype Design Pattern

Definition

Purpose

This pattern is useful when the cost of creating a new instance of an object is more expensive than copying an existing one.

Use cases

1. When object creation is costly or complex

Example: In scenarios where creating a new object is resource-intensive (due to expensive database calls, network requests, or complex initial setup), the Prototype pattern allows you to clone an existing object. This avoids unnecessary repetition of the heavy creation process.

Use Case: Creating a large number of similar objects in a graphic editor, where each object has similar properties, can be optimized by cloning.

Design

Example

Let’s illustrate this with a simple example involving shapes.

Implementation

  1. Create the Prototype Interface
public interface Shape {
    Shape clone();
    void draw();
}
  1. Create Concrete Prototype Classes
public class Circle implements Shape {
    @Override
    public Shape clone() {
        return new Circle();
    }

    @Override
    public void draw() {
        System.out.println("Drawing a Circle");
    }
}
public class Square implements Shape {
    @Override
    public Shape clone() {
        return new Square();
    }

    @Override
    public void draw() {
        System.out.println("Drawing a Square");
    }
}
  1. Client code to demonstrate the usage
public static void main(String[] args) {
        // Create a circle and square
        Shape circle = new Circle();
        Shape square = new Square();
        
        // Clone the shapes
        Shape clonedCircle = circle.clone();
        Shape clonedSquare = square.clone();
        
        // Draw original and cloned shapes
        circle.draw(); // Output: Drawing a Circle
        clonedCircle.draw(); // Output: Drawing a Circle
        
        square.draw(); // Output: Drawing a Square
        clonedSquare.draw(); // Output: Drawing a Square
    
        System.out.println(circle==clonedCircle); // false, different objects
}