Module A2 — Creational Patterns
Complete reference notes · Track A: LLD · Week 4
Creational design patterns abstract the instantiation process. They help make a system independent of how its objects are created, composed, and represented. As systems evolve, they often rely more on object composition than class inheritance, shifting the emphasis away from hard-coding a fixed set of behaviours toward defining a smaller set of fundamental behaviours that can be composed into any number of more complex ones. Thus, creating objects with specific behaviours requires more than simply instantiating a class.
1. Singleton
Definition: Ensure a class has only one instance, and provide a global point of access to it.
When to Use:
- When there must be exactly one instance of a class, and it must be accessible to clients from a well-known access point (e.g., a central Logger, a configuration manager, a database connection pool).
- When the sole instance should be extensible by subclassing, and clients should be able to use an extended instance without modifying their code.
Implementation: Enum Singleton (Preferred in Java)
Joshua Bloch (Effective Java) recommends the Enum approach. It provides built-in serialization machinery, guarantees against multiple instantiations, and handles reflection attacks perfectly.
public enum ConfigManager {
INSTANCE;
private Map<String, String> properties;
ConfigManager() {
properties = new HashMap<>(); // load from file
}
public String getProperty(String key) {
return properties.get(key);
}
}
Implementation: Double-Checked Locking (Thread-Safe)
If you explicitly need lazy initialization and cannot use Enums.
public class DatabasePool {
private static volatile DatabasePool instance;
private DatabasePool() {
// private constructor prevents instantiation
}
public static DatabasePool getInstance() {
if (instance == null) { // 1st check (no lock, fast-path)
synchronized (DatabasePool.class) {
if (instance == null) { // 2nd check (safe)
instance = new DatabasePool();
}
}
}
return instance;
}
}
Note: The volatile keyword is crucial. It ensures that multiple threads handle the instance variable correctly when it is being initialized.
SOLID Impact: Singleton
Violates SRP: The class manages its own lifecycle AND performs its primary business logic.
Recommendation: In modern architectures (e.g., Spring framework), use Dependency Injection containers to manage the "singleton" scope of an object, rather than hardcoding the Singleton pattern structurally.
2. Factory Method
Definition: Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.
When to Use:
- A class can’t anticipate the class of objects it must create.
- A class wants its subclasses to specify the objects it creates.
- Classes delegate responsibility to one of several helper subclasses, and you want to localize the knowledge of which helper subclass is the delegate.
Real-world analogy: A logistics company has a TransportBuilder. Its subclasses RoadLogistics and SeaLogistics decide whether to create a Truck or a Ship.
Implementation Example
// Product Interface
interface Notification {
void send(String message);
}
// Concrete Products
class EmailNotification implements Notification {
public void send(String msg) { System.out.println("Emailing: " + msg); }
}
class PushNotification implements Notification {
public void send(String msg) { System.out.println("Pushing: " + msg); }
}
// Creator (The Factory)
abstract class NotificationCreator {
public abstract Notification createNotification(); // Factory Method
// Core business logic relying on the product
public void broadcast(String msg) {
Notification notification = createNotification();
notification.send(msg);
}
}
// Concrete Creators
class EmailCreator extends NotificationCreator {
public Notification createNotification() { return new EmailNotification(); }
}
class PushCreator extends NotificationCreator {
public Notification createNotification() { return new PushNotification(); }
}
3. Abstract Factory
Definition: Provide an interface for creating families of related or dependent objects without specifying their concrete classes.
Difference from Factory Method:
- Factory Method creates one product. It uses inheritance.
- Abstract Factory creates a family of related products. It typically uses composition to delegate creation methods to different Factory objects.
When to Use:
- A system should be independent of how its products are created, composed, and represented.
- A system should be configured with one of multiple families of products (e.g., Mac UI vs. Windows UI).
- You want to provide a class library of products, and you want to reveal just their interfaces, not their implementations.
Implementation Example
// Abstract Factory
interface GUIFactory {
Button createButton();
Checkbox createCheckbox();
}
// Concrete Factory 1: Mac
class MacFactory implements GUIFactory {
public Button createButton() { return new MacButton(); }
public Checkbox createCheckbox() { return new MacCheckbox(); }
}
// Concrete Factory 2: Win
class WinFactory implements GUIFactory {
public Button createButton() { return new WinButton(); }
public Checkbox createCheckbox() { return new WinCheckbox(); }
}
// Client Code: Injects the factory
class Application {
private Button button;
public Application(GUIFactory factory) {
button = factory.createButton(); // Client doesn't care if it's Mac or Win
}
}
4. Builder
Definition: Separate the construction of a complex object from its representation so that the same construction process can create different representations.
When to Use:
- The algorithm for creating a complex object should be independent of the parts that make up the object and how they’re assembled.
- The construction process must allow different representations for the object that’s constructed.
- To solve the “Telescoping Constructor” anti-pattern (constructors with many optional parameters).
Implementation
public class UserProfile {
// Final fields make the object immutable
private final String firstName; // Required
private final String lastName; // Required
private final int age; // Optional
private final String phone; // Optional
private final String address; // Optional
private UserProfile(Builder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.age = builder.age;
this.phone = builder.phone;
this.address = builder.address;
}
public static class Builder {
private final String firstName;
private final String lastName;
private int age = 0; // Default optional
private String phone = ""; // Default optional
private String address = ""; // Default optional
public Builder(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Builder phone(String phone) {
this.phone = phone;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public UserProfile build() {
// Validation logic goes here before object creation
if(age < 0) throw new IllegalArgumentException("Age cannot be negative");
return new UserProfile(this);
}
}
}
// Usage:
UserProfile user = new UserProfile.Builder("Ajay", "Dev")
.age(28)
.address("123 Tech Lane")
.build();
5. Prototype
Definition: Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
When to Use:
- When the classes to instantiate are specified at run-time (dynamic loading).
- To avoid building a class hierarchy of factories that parallels the class hierarchy of products.
- When instances of a class can have one of only a few different combinations of state. It may be more convenient to install a corresponding number of prototypes and clone them rather than instantiating the class manually each time.
Implementation (Deep vs Shallow Clone)
Java’s default clone() method provides a shallow copy (references to nested objects are shared). In System Design, you usually want a deep copy, executed manually via a Copy Constructor.
abstract class Shape {
public int x, y;
public String color;
// Copy constructor
public Shape(Shape target) {
if (target != null) {
this.x = target.x;
this.y = target.y;
this.color = target.color;
}
}
public abstract Shape clone();
}
class Circle extends Shape {
public int radius;
public Circle(Circle target) {
super(target); // Copy parent properties
if (target != null) {
this.radius = target.radius;
}
}
@Override
public Shape clone() {
return new Circle(this); // Passes itself to copy constructor
}
}
// Registry used to cache prototypes
class ShapeRegistry {
private Map<String, Shape> cache = new HashMap<>();
public ShapeRegistry() {
Circle circle = new Circle(null);
circle.x = 10; circle.y = 10; circle.radius = 20; circle.color = "Red";
cache.put("Big Red Circle", circle);
} // Create expensive object ONCE
public Shape get(String key) {
return cache.get(key).clone(); // Return cloned instances cheaply
}
}
Comparison Summary Table
| Pattern | Creates | Mechanism | Primary SOLID Principle Enforced |
|---|---|---|---|
| Singleton | A single, globally accessible instance | Private constructor + static accessor | SRP (Though it often violates it practically, conceptually it manages one state globally). |
| Factory Method | One specific product object | Subclass overrides a creator method | OCP (Add new creators without modifying existing ones). |
| Abstract Factory | A family of related product objects | Interface injection with multiple factory methods | OCP and ISP (Interface Segregation). |
| Builder | A complex object, step-by-step | Inner Builder class, chained setters, `build()` method | SRP (Separates construction logic from the data model). |
| Prototype | A clone of an existing object | `clone()` interfaces and copy constructors | OCP (Cloning avoids concrete dependencies on classes). |