Creational design patterns are a subset of design patterns in software engineering that focus on the process of object creation. These patterns provide various ways to instantiate objects while hiding the complexities involved in the process. Creational design patterns promote flexibility, reusability, and maintainability in your codebase. In this article, we'll explore some of the most commonly used creational design patterns with java examples.

Abstract Factory Pattern

Provides an interface for creating families of related or dependent objects without specifying their concrete classes.

Example

interface IShape {
    void draw();
}

abstract class AbstractShapeFactory {
    abstract IShape getShape(String targetShape);
}

class Circle implements IShape {
    @Override
    public void draw() {
        System.out.println("Drawing circle.");
    }
}

class Square implements IShape {
    @Override
    public void draw() {
        System.out.println("Drawing square.");
    }
}

class Rectangle implements IShape {
    @Override
    public void draw() {
        System.out.println("Drawing rectangle.");
    }
}

class NormalShapeFactory extends AbstractShapeFactory {
    @Override
    IShape getShape(String targetShape) {
        return switch(targetShape.toLowerCase()) {
            case "circle" -> new Circle();
            case "square" -> new Square();
            case "rectangle" -> new Rectangle();
            default -> null;
        };
    }
}

class RoundedSquare implements IShape {
    @Override
    public void draw() {
        System.out.println("Drawing rounded square.");
    }
}

class RoundedRectangle implements IShape {
    @Override
    public void draw() {
        System.out.println("Drawing rounded rectangle.");
    }
}

class RoundedShapeFactory extends AbstractShapeFactory {
    @Override
    IShape getShape(String targetShape) {
        return switch(targetShape.toLowerCase()) {
            case "circle" -> new Circle();
            case "square" -> new RoundedSquare();
            case "rectangle" -> new RoundedRectangle();
            default -> null;
        };
    }
}

class ShapeFactoryProducer {
    public static AbstractShapeFactory getFactory(String shapeType){
        return switch (shapeType.toLowerCase()) {
            case "rounded" -> new RoundedShapeFactory();
            default -> new NormalShapeFactory();
        };
    }
}

public class Main {
    public static void main(String[] args) {
        AbstractShapeFactory normalShapeFactory = ShapeFactoryProducer.getFactory("normal");
        IShape normalSquare = normalShapeFactory.getShape("square");
        normalSquare.draw();

        AbstractShapeFactory roundedShapeFactory = ShapeFactoryProducer.getFactory("rounded");
        IShape roundedSquare = roundedShapeFactory.getShape("square");
        roundedSquare.draw();
    }
}

Builder Pattern

Separates the construction of a complex object (i.e. requires a lot of constructors and properties) from its representation, allowing the same construction process to create different representations.

Example

class Product {
    private String part1;
    private String part2;

    public void setPart1(String part1) {
        this.part1 = part1;
    }

    public void setPart2(String part2) {
        this.part2 = part2;
    }

    public void show() {
        System.out.println("Part 1: " + part1);
        System.out.println("Part 2: " + part2);
    }
}

class ProductBuilder {
    private String part1;
    private String part2;

    public ProductBuilder buildPart1(String part1) {
        this.part1 = part1;
        return this;
    }

    public ProductBuilder buildPart2(String part2) {
        this.part2 = part2;
        return this;
    }

    public Product build() {
        Product product = new Product();
        product.setPart1(part1);
        product.setPart2(part2);
        return product;
    }
}

public class Main {
    public static void main(String[] args) {
        ProductBuilder builder = new ProductBuilder();
        Product product = builder
            .buildPart1("Part 1")
            .buildPart2("Part 2")
            .build();

        product.show();
    }
}

Factory Pattern

Defines an interface for creating an object but allows subclasses to alter the type of objects that will be created.

Example

interface IShape {
    void draw();
}

interface IShapeFactory {
    IShape createShape(String shapeType);
}

class Circle implements IShape {
    @Override
    public void draw() {
        System.out.println("Drawing circle.");
    }
}

class Rectangle implements IShape {
    @Override
    public void draw() {
        System.out.println("Drawing rectangle.");
    }
}

class ShapeFactory implements IShapeFactory {
    @Override
    public IShape createShape(String shapeType) {
        if (shapeType.equals("circle")) {
            return new Circle();
        }
        else if (shapeType.equals("rectangle")) {
            return new Rectangle();
        }
        else {
            throw new IllegalArgumentException("Invalid shape type: " + shapeType);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        ShapeFactory shapeFactory = new ShapeFactory();
        IShape shape = shapeFactory.createShape("circle");
        System.out.println(shape);
    }
}

Lazy Initialization Pattern

Delays the creation of an object or the calculation of a value until it is actually needed.

Example

class LightObject1 {
    public LightObject1() {
        System.out.println("LightObject1 initialized");
    }
}

class LightObject2 {
    public LightObject2() {
        System.out.println("LightObject2 initialized");
    }
}

class ExpensiveObject {
    public ExpensiveObject() {
        // Simulate a time-consuming initialization process
        try {
            Thread.sleep(2000); // Sleep for 2 seconds to simulate initialization time
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("ExpensiveObject initialized");
    }
}

class LazyInitialization {
    // Lightweight objects to be instantiated in the constructor
    private LightObject1 lightObject1;
    private LightObject2 lightObject2;

    // Declare a private field to hold the lazily initialized object
    private ExpensiveObject expensiveObject;

    public LazyInitialization() {
        // Instantiate the lightweight objects in the constructor
        lightObject1 = new LightObject1();
        lightObject2 = new LightObject2();
    }

    // This method initializes and returns the expensive object on-demand
    public ExpensiveObject getExpensiveObject() {
        if (expensiveObject == null) {
            // Lazy initialization: Create the expensive object when it's first requested
            expensiveObject = new ExpensiveObject();
        }
        return expensiveObject;
    }
}

public class Main {
    public static void main(String[] args) {
        LazyInitialization example = new LazyInitialization();

        // At this point, the lightweight objects are already instantiated,
        // but the expensive object has not been created yet
        System.out.println("Lightweight objects initialized");
        System.out.println("Expensive object not yet initialized");

        // When needed, get the expensive object
        ExpensiveObject obj = example.getExpensiveObject();

        // Now, the expensive object has been initialized
        System.out.println("Expensive object initialized");
    }
}

Prototype Pattern

Creates new objects by copying an existing object, known as the prototype, rather than instantiating new objects using constructors.

Example

class Prototype implements Cloneable {
    private String name;

    public Prototype(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public Prototype clone() throws CloneNotSupportedException {
        return (Prototype) super.clone();
    }
}

public class Main {

    public static void main(String ... args) {
        Prototype prototype1 = new Prototype("Prototype 1");

        try {
            Prototype prototype2 = prototype1.clone();
            System.out.println("Clone: " + prototype2.getName()); // Output: Clone: Prototype 1
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

Singleton Pattern

Ensures that a class has only one instance and provides a global point of access to that instance.

Example

final class Singleton {
    private static Singleton instance;

    private Singleton() {
        // Private constructor to prevent instantiation
    }

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

public class Main {
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        System.out.println(singleton1);
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton2);
    }
}