The most surprising truth about design patterns like Builder, Factory Method, and Abstract Factory is that they aren’t about creating objects, but about delegating the responsibility of creation to a specialized entity, often in a way that decouples the client from the concrete implementation details.
Let’s see this in action. Imagine we’re building a simple Computer object.
// Product
class Computer {
private String cpu;
private String ram;
private String storage;
private boolean graphicsCard;
// Private constructor, only accessible by Builder
private Computer(ComputerBuilder builder) {
this.cpu = builder.cpu;
this.ram = builder.ram;
this.storage = builder.storage;
this.graphicsCard = builder.graphicsCard;
}
@Override
public String toString() {
return "Computer [CPU=" + cpu + ", RAM=" + ram + ", Storage=" + storage + ", GraphicsCard=" + graphicsCard + "]";
}
// Builder static inner class
public static class ComputerBuilder {
private String cpu;
private String ram;
private String storage;
private boolean graphicsCard;
public ComputerBuilder setCpu(String cpu) {
this.cpu = cpu;
return this;
}
public ComputerBuilder setRam(String ram) {
this.ram = ram;
return this;
}
public ComputerBuilder setStorage(String storage) {
this.storage = storage;
return this;
}
public ComputerBuilder setGraphicsCard(boolean graphicsCard) {
this.graphicsCard = graphicsCard;
return this;
}
public Computer build() {
// Optional: Add validation here before building
if (this.cpu == null || this.ram == null) {
throw new IllegalArgumentException("CPU and RAM are required.");
}
return new Computer(this);
}
}
}
// Client code
public class BuilderDemo {
public static void main(String[] args) {
Computer gamingComputer = new Computer.ComputerBuilder()
.setCpu("Intel i9")
.setRam("32GB DDR4")
.setStorage("1TB NVMe SSD")
.setGraphicsCard(true)
.build();
Computer officeComputer = new Computer.ComputerBuilder()
.setCpu("Intel i5")
.setRam("8GB DDR4")
.setStorage("256GB SSD")
.setGraphicsCard(false)
.build();
System.out.println("Gaming Computer: " + gamingComputer);
System.out.println("Office Computer: " + officeComputer);
}
}
This ComputerBuilder example illustrates the core problem the Builder pattern solves: constructing complex objects with many optional parameters, or objects that require a specific step-by-step construction process, without cluttering the constructor with dozens of arguments. The builder encapsulates the construction logic, allowing the client to configure the object incrementally and in any order, only calling build() when ready. The product’s constructor is often made private to enforce this controlled creation process.
Now, let’s consider the Factory Method pattern. Its purpose is to define an interface for creating an object, but let subclasses decide which class to instantiate.
// Product interface
interface Notification {
void send(String message);
}
// Concrete Products
class EmailNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Sending Email: " + message);
}
}
class SMSNotification implements Notification {
@Override
public void send(String message) {
System.out.println("Sending SMS: " + message);
}
}
// Creator abstract class
abstract class NotificationService {
// The Factory Method
public abstract Notification createNotification();
public void dispatchNotification(String message) {
Notification notification = createNotification(); // Calls the factory method
notification.send(message);
}
}
// Concrete Creators
class EmailNotificationService extends NotificationService {
@Override
public Notification createNotification() {
return new EmailNotification();
}
}
class SMSNotificationService extends NotificationService {
@Override
public Notification createNotification() {
return new SMSNotification();
}
}
// Client code
public class FactoryMethodDemo {
public static void main(String[] args) {
NotificationService emailService = new EmailNotificationService();
emailService.dispatchNotification("Your order has shipped!");
NotificationService smsService = new SMSNotificationService();
smsService.dispatchNotification("Your verification code is 12345.");
}
}
Here, NotificationService declares the createNotification() factory method, but it’s the subclasses (EmailNotificationService, SMSNotificationService) that implement it to return specific types of Notification. The dispatchNotification method uses the abstract factory method, making it oblivious to whether it’s dispatching an email or an SMS. This is powerful for creating families of objects where subclasses determine the exact instance.
The Abstract Factory pattern takes this a step further. It provides an interface for creating families of related or dependent objects without specifying their concrete classes.
// Abstract Products
interface Button {
void render();
}
interface Checkbox {
void render();
}
// Concrete Products for macOS
class MacOSButton implements Button {
@Override
public void render() {
System.out.println("Rendering macOS Button");
}
}
class MacOSCheckbox implements Checkbox {
@Override
public void render() {
System.out.println("Rendering macOS Checkbox");
}
}
// Concrete Products for Windows
class WindowsButton implements Button {
@Override
public void render() {
System.out.println("Rendering Windows Button");
}
}
class WindowsCheckbox implements Checkbox {
@Override
public void render() {
System.out.println("Rendering Windows Checkbox");
}
}
// Abstract Factory
interface GUIFactory {
Button createButton();
Checkbox createCheckbox();
}
// Concrete Factories
class MacOSFactory implements GUIFactory {
@Override
public Button createButton() {
return new MacOSButton();
}
@Override
public Checkbox createCheckbox() {
return new MacOSCheckbox();
}
}
class WindowsFactory implements GUIFactory {
@Override
public Button createButton() {
return new WindowsButton();
}
@Override
public Checkbox createCheckbox() {
return new WindowsCheckbox();
}
}
// Client code
public class AbstractFactoryDemo {
public static void main(String[] args) {
GUIFactory factory = new WindowsFactory(); // Or new MacOSFactory();
renderUI(factory);
System.out.println("\n--- Switching OS ---");
factory = new MacOSFactory();
renderUI(factory);
}
public static void renderUI(GUIFactory factory) {
Button button = factory.createButton();
Checkbox checkbox = factory.createCheckbox();
button.render();
checkbox.render();
}
}
In the Abstract Factory example, GUIFactory is an interface that defines methods for creating multiple related products (createButton, createCheckbox). Concrete factories like MacOSFactory and WindowsFactory implement this interface to produce a consistent set of products for a specific platform. The client renderUI method is given a factory and uses it to create and interact with buttons and checkboxes, ensuring that all UI elements created by that factory belong to the same family (e.g., all macOS-styled or all Windows-styled).
When to use which?
- Builder: Use when the construction of an object is complex, involves many optional parameters, or requires a step-by-step process. It’s ideal for creating immutable objects or when you want to separate the construction logic from the object’s business logic. Think of building a
Computeror a complexCarconfiguration. - Factory Method: Use when a class cannot anticipate the class of objects it must create. It allows subclasses to specify the exact type of object to be instantiated. This is useful for frameworks or libraries where you want to allow users to extend the system with their own product types. Consider creating different
Notificationtypes or differentDocumenttypes in an application. - Abstract Factory: Use when you need to create families of related or dependent objects, and you want to ensure that the created objects are compatible with each other. It’s about creating a group of objects that work together. This is common when dealing with different "themes" or "platforms," like the
GUIFactoryexample, or creating different sets of database access objects for different database systems.
A subtle point: Abstract Factory often uses Factory Methods internally to create its individual products, but its primary role is orchestrating the creation of the entire family of products.
The next logical step is to explore how these patterns can be combined, particularly how Abstract Factories might delegate to Builder patterns for their complex products.