Instantiation logic often becomes tightly coupled with business requirements when objects are created directly using the new keyword. Modifying these dependencies later requires widespread code changes, violating the Open/Closed Principle. Creational design patterns abstract the object creation process, centralizing initialization logic and enhancing system flexibility. This section covers four primary approaches: Simple Factory, Factory Method, Abstract Factory, Prototype, and Builder.
Simple Factory Idiom
Though not part of the official GoF catalog, this approach serves as a practical programming habit. It consolidates instance creation into a single utility class that branches based on input parameters.
public enum ProductType { STANDARD, PREMIUM }
public class SimpleProducer {
public static Object generate(ProductType type) {
return switch (type) {
case STANDARD -> new StandardComponent();
case PREMIUM -> new PremiumComponent();
default -> throw new IllegalArgumentException("Unknown type");
};
}
}
While this centralizes creation, adding new variants forces modifications to the factory itself, breaking open/closed compliance. A common refinement involves leveraging external configuration files combined with reflection to instantiate classes dynamically at runtime, effectively decoupling the producer from concrete implementations.
Factory Method Pattern
To address the rigidity of the simple factory, the Factory Method pattern delegates instantiation to specialized subclasses. Each subclass defines how a specific product variant is constructed.
Core components include:
Creator: Declares the factory method returning an abstract product.ConcreteCreator: Overrides the factory method to produce a specificConcreteProduct.Product: Interface defining operations common to all derived objects.
interface Logger { void log(String message); }
class FileLogger implements Logger {
public void log(String msg) { /* file writing logic */ }
}
class ConsoleLogger implements Logger {
public void log(String msg) { System.out.println(msg); }
}
abstract class LoggerFactory {
public abstract Logger createLogger();
public void printReport(String data) {
Logger logger = createLogger(); // Delegates to subclass
logger.log(data);
}
}
class FileLoggerFactory extends LoggerFactory {
@Override public Logger createLogger() { return new FileLogger(); }
}
class ConsoleLoggerFactory extends LoggerFactory {
@Override public Logger createLogger() { return new ConsoleLogger(); }
}
Introducing a new logging target requires only adding a new Logger implementation and its corresponding factory subclass, leaving existing code untouched. The trade-off is an increase in class count proportional to the number of product variants.
Abstract Factory Pattern
When systems require families of related or dependent objects without specifying their concrete classes, the Abstract Factory pattern provides a cohesive interface for generating entire product sets.
Consider a hardware vendor producing motherboards and RAM sticks. Both are product families with multiple quality tiers.
interface Motherboard {}
interface RAMModule {}
class ZSeriesMotherboard implements Motherboard {}
class XPlusRAM implements RAMModule {};
class ABaseMotherboard implements Motherboard {}
class D4Stick implements RAMModule {}
interface HardwareSupplier {
Motherboard createBoard();
RAMModule createRam();
}
class HighEndSupplier implements HardwareSupplier {
public Motherboard createBoard() { return new ZSeriesMotherboard(); }
public RAMModule createRam() { return new XPlusRAM(); }
}
class LowCostSupplier implements HardwareSupplier {
public Motherboard createBoard() { return new ABaseMotherboard(); }
public RAMModule createRam() { return new D4Stick(); }
}
Clients interact solely with the HardwareSupplier interface. Switching from high-end to budget components requires instantiating a different supplier factory. Adding a new product line forces modifications across every concrete supplier class, making this pattern best suited for stable product hierarchies.
Prototype Pattern
Instead of instantiating objects through constructors, the Prototype pattern creates new instances by cloning an existing prototype. This is particularly useful when object initialization is resource-intensive or when the exact class of the object to be created must be determined at runtime.
Java natively supports this via the Cloneable interface and the clone() method inherited from java.lang.Object. Cloning operates in two modes:
- Shallow Copy: Copies primitive fields and reference pointers. Nested objects remain shared between the original and the clone.
- Deep Copy: Recursively duplicates referenced objects, ensuring complete independence between instances.
Basic shallow cloning example:
class DocumentTemplate implements Cloneable {
private String title;
private Date generatedAt;
public DocumentTemplate clone() throws CloneNotSupportedException {
return (DocumentTemplate) super.clone();
}
}
For scenarios requiring deep copying, serialization-based approaches bypass reference sharing entirely. By writing the object graph to a byte stream and reading it back, all nested structures are reconstructed as independent heap allocations.
import java.io.*;
class Report implements Serializable {
private ItemDetails details;
public Report deepClone() {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(this);
try (ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(bos.toByteArray()))) {
return (Report) ois.readObject();
}
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException("Cloning failed", e);
}
}
}
This technique guarantees that modifying properties in the cloned report does not inadvertently alter the original template.
Builder Pattern
Constructing complex entities step-by-step decouples algorithm execution from representation. The Builder pattern separates object construction logic from the final assembly process, enabling identical construction sequences to yield varied outcomes.
Key participants:
Builder: Defines an interface for creating parts of aProduct.ConcreteBuilder: Implements construction steps and tracks the resulting product.Director: Orchestrates the building sequence using theBuilderinterface.Product: The complex object being assembled.
Example: Configuring server specifications.
class ServerSpecs {
private String cpu; private String ram; private String storage;
ServerSpecs(String c, String r, String s) { cpu=c; ram=r; storage=s; }
}
abstract class ServerBuilder {
protected ServerSpecs specs = new ServerSpecs("", "", "");
public abstract void configureCPU();
public abstract void configureStorage();
public abstract ServerSpecs getResult();
}
class WorkstationBuilder extends ServerBuilder {
public void configureCPU() { specs.cpu = "AMD Threadripper"; }
public void configureStorage() { specs.storage = "NVMe RAID 0"; }
}
class BasicServerBuilder extends ServerBuilder {
public void configureCPU() { specs.cpu = "Intel i5"; }
public void configureStorage() { specs.storage = "SATA SSD"; }
}
class SystemArchitect {
private ServerBuilder builder;
public SystemArchitect(ServerBuilder b) { builder = b; }
public ServerSpecs build() {
builder.configureCPU();
builder.configureStorage();
return builder.getResult();
}
}
In modern Java development, the Builder pattern is frequently adapted into a fluent API via nested static builder classes within the target entity. This eliminates the need for a separate Director class and improves readability by chaining method cals.
class ConfigurableService {
private String apiKey;
private int timeoutMs;
private boolean encryptionEnabled;
private ConfigurableService(Builder builder) {
this.apiKey = builder.key;
this.timeoutMs = builder.timeout;
this.encryptionEnabled = builder.secure;
}
public static class Builder {
private String key;
private int timeout = 5000;
private boolean secure = false;
public Builder setKey(String k) { this.key = k; return this; }
public Builder setTimeout(int ms) { this.timeout = ms; return this; }
public Builder enableEncryption(boolean flag) { this.secure = flag; return this; }
public ConfigurableService build() {
return new ConfigurableService(this);
}
}
}
This approach excels when dealing with constructors possessing numerous optional parameters, eliminating telescoping constructor anti-patterns.
Pattern Selection Guidelines
Choosing the appropriate creational strategy depends on architectural needs:
- Use Factory Method when a class cannot anticipate the classes of objects it must create, or when you want subclasses to specify instantiation.
- Opt for Abstract Factory when systems must work with families of related objects without exposing dependencies, such as theming engines or cross-platform GUI toolkits.
- Select Prototype when instantiation costs are high, or when generating dynamic variations of persistent records.
- Apply Builder when object initialization involves many configurable parameters, or when construction must proceed through a strict sequence of dependent steps.
While Factory patterns emphasize what gets created, Builder patterns focus on how it gets assembled. Understanding these distinctions allows developers to map problem domains accurately to structural solutions, resulting in maintainable and extensible codebases.