Understanding Creational Design Patterns in Java

Better Dev XPBetter Dev XP
5 min read

Creational design patterns focus on object creation mechanisms, abstracting the instantiation process, and making the system independent of how its objects are composed, represented, and varied. In Java, these patterns play a crucial role in promoting flexibility, reusability, and maintainability. In this article, we'll explore some prominent creational design patterns in Java, providing insights into their usage and benefits.

Singleton Pattern

The Singleton Pattern ensures that a class has only one instance and provides a global point of access to that instance. This pattern is beneficial when exactly one object is needed to coordinate actions across the system.

public class Singleton {
    private static Singleton instance;

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

    // Lazy initialization with double-check locking
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Real-world Scenario: Database Connection

In scenarios where creating multiple database connections is unnecessary or resource-intensive, a singleton pattern can be applied to ensure that there's only one instance of the database connection manager. This can help manage resources efficiently.

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

    private DatabaseConnectionManager() {
        // Initialization code
    }

    public static DatabaseConnectionManager getInstance() {
        return instance;
    }

    // Other database-related methods...
}

Factory Method Pattern

The Factory Method Pattern defines an interface for creating an object but lets subclasses alter the type of objects that will be created. It provides an interface for creating instances in a superclass, but the exact type of objects is deferred to the subclasses.

interface Product {
    void create();
}

class ConcreteProductA implements Product {
    @Override
    public void create() {
        System.out.println("Product A created.");
    }
}

class ConcreteProductB implements Product {
    @Override
    public void create() {
        System.out.println("Product B created.");
    }
}

abstract class Creator {
    // Factory method
    abstract Product factoryMethod();

    // Other methods in the class
}

class ConcreteCreatorA extends Creator {
    @Override
    Product factoryMethod() {
        return new ConcreteProductA();
    }
}

class ConcreteCreatorB extends Creator {
    @Override
    Product factoryMethod() {
        return new ConcreteProductB();
    }
}

Real-world Scenario: Document Conversion

Consider a document conversion system where different document types (PDF, Word, Excel) need to be converted to a common format. The factory method pattern can be applied to create document converters for each type.

interface Document {
    void convert();
}

class PDFDocument implements Document {
    @Override
    public void convert() {
        System.out.println("Converting PDF to common format...");
    }
}

class WordDocument implements Document {
    @Override
    public void convert() {
        System.out.println("Converting Word to common format...");
    }
}

class ExcelDocument implements Document {
    @Override
    public void convert() {
        System.out.println("Converting Excel to common format...");
    }
}

abstract class DocumentConverter {
    abstract Document createDocument();
}

class PDFConverter extends DocumentConverter {
    @Override
    Document createDocument() {
        return new PDFDocument();
    }
}

class WordConverter extends DocumentConverter {
    @Override
    Document createDocument() {
        return new WordDocument();
    }
}

class ExcelConverter extends DocumentConverter {
    @Override
    Document createDocument() {
        return new ExcelDocument();
    }
}

Abstract Factory Pattern

The Abstract Factory Pattern provides an interface for creating families of related or dependent objects without specifying their concrete classes. It is particularly useful when a system should be independent of how its objects are created, composed, and represented.

// Abstract product interfaces
interface CPU {
    String getArchitecture();
}

interface RAM {
    String getType();
}

// Concrete product classes
class IntelCPU implements CPU {
    @Override
    public String getArchitecture() {
        return "Intel Architecture";
    }
}

class SamsungRAM implements RAM {
    @Override
    public String getType() {
        return "Samsung RAM";
    }
}

// Abstract Factory interface
interface ComputerFactory {
    CPU createCPU();
    RAM createRAM();
}

// Concrete Factory
class HighPerformanceComputerFactory implements ComputerFactory {
    @Override
    public CPU createCPU() {
        return new IntelCPU();
    }

    @Override
    public RAM createRAM() {
        return new SamsungRAM();
    }
}

Real-world Scenario: GUI Library

In a graphical user interface (GUI) library, the abstract factory pattern can be used to create platform-specific UI components. For instance, a ButtonFactory can produce buttons with native styles for Windows, macOS, and Linux.

// Abstract product interfaces
interface Button {
    void render();
}

interface Window {
    void setTitle(String title);
    void renderWindow();
}

// Concrete product classes for Windows
class WindowsButton implements Button {
    @Override
    public void render() {
        System.out.println("Rendering Windows-style button");
    }
}

class WindowsWindow implements Window {
    @Override
    public void setTitle(String title) {
        System.out.println("Setting title for Windows window: " + title);
    }

    @Override
    public void renderWindow() {
        System.out.println("Rendering Windows-style window");
    }
}

// Concrete Factory for Windows
class WindowsUIFactory implements UIFactory {
    @Override
    public Button createButton() {
        return new WindowsButton();
    }

    @Override
    public Window createWindow() {
        return new WindowsWindow();
    }
}

// Client code
UIFactory factory = new WindowsUIFactory();
Button button = factory.createButton();
Window window = factory.createWindow();

Builder Pattern

The Builder Pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations. It is useful when an object has a large number of parameters or configurations.

class Product {
    private String part1;
    private String part2;

    // Private constructor
    private Product() {}

    // Setters (optional, depending on immutability requirements)

    @Override
    public String toString() {
        return "Product{" +
                "part1='" + part1 + '\'' +
                ", part2='" + part2 + '\'' +
                '}';
    }

    // Builder class
    static class Builder {
        private Product product = new Product();

        Builder withPart1(String part1) {
            product.part1 = part1;
            return this;
        }

        Builder withPart2(String part2) {
            product.part2 = part2;
            return this;
        }

        Product build() {
            return product;
        }
    }
}

public class Client {
    public static void main(String[] args) {
        Product product = new Product.Builder()
                .withPart1("Part 1")
                .withPart2("Part 2")
                .build();

        System.out.println(product);
    }
}

Real-world Scenario: Report Generation

Consider a scenario where reports with varying structures and contents need to be generated. The builder pattern can be used to create a ReportBuilder which allows the flexible construction of different types of reports.

class Report {
    private String title;
    private String header;
    private String content;

    // Private constructor
    private Report() {}

    // Getters...

    @Override
    public String toString() {
        return "Report{" +
                "title='" + title + '\'' +
                ", header='" + header + '\'' +
                ", content='" + content + '\'' +
                '}';
    }

    // Builder class
    static class Builder {
        private final Report report = new Report();

        Builder withTitle(String title) {
            report.title = title;
            return this;
        }

        Builder withHeader(String header) {
            report.header = header;
            return this;
        }

        Builder withContent(String content) {
            report.content = content;
            return this;
        }

        Report build() {
            return report;
        }
    }
}

public class ReportGenerator {
    public static void main(String[] args) {
        Report report = new Report.Builder()
                .withTitle("Monthly Sales Report")
                .withHeader("Sales Summary")
                .withContent("This report summarizes the monthly sales data.")
                .build();

        System.out.println(report);
    }
}
0
Subscribe to my newsletter

Read articles from Better Dev XP directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Better Dev XP
Better Dev XP