46.2 Creational Patterns: Factory, Abstract Factory, and Builder
Right, creational patterns. This is where we stop just slapping new everywhere like it’s going out of style and start thinking about how objects get made. Because trust me, how they get made matters. It’s the difference between a tangled mess of dependencies and code that’s flexible enough to actually survive contact with the real world. Let’s break down the big three.
The Factory Method: Your Personal Object Shopper
Think of the Factory Method not as a giant, concrete factory, but as a dedicated personal shopper for objects. You don’t go to the store yourself; you just tell your shopper what you need, and they come back with the right thing. In code, this means we define an interface for creating an object, but we let the subclasses decide which exact class to instantiate. The “how” is abstracted away.
Why would you do this? Imagine you’re building a UI framework. You have a Button interface, but the actual WindowsButton and MacOSButton look and behave very differently. Your core dialog box code shouldn’t be riddled with if (os === 'windows') statements. That’s a maintenance nightmare. The Factory Method solves this by pushing that decision down.
// The product interface everyone agrees to
interface Button {
render(): void;
onClick(callback: () => void): void;
}
// Concrete products
class WindowsButton implements Button {
render() { console.log("Rendering a Windows-style button"); }
onClick(callback: () => void) { /* Windows-specific click logic */ }
}
class MacOSButton implements Button {
render() { console.log("Rendering a gorgeous Mac-style button"); }
onClick(callback: () => void) { /* Mac-specific click logic */ }
}
// The creator class that houses the factory method
abstract class Dialog {
// This is the factory method. It's abstract, so subclasses MUST implement it.
abstract createButton(): Button;
render() {
// See? The core business logic uses the factory method.
// It has no idea what concrete button it's getting.
const okButton = this.createButton();
okButton.render();
}
}
// Concrete creators override the factory method.
class WindowsDialog extends Dialog {
createButton(): Button {
return new WindowsButton();
}
}
class MacOSDialog extends Dialog {
createButton(): Button {
return new MacOSButton();
}
}
// Usage: The client code works with instances of a concrete creator, but through the Dialog interface.
// The OS check happens exactly once, at the application's initialization root.
const dialog: Dialog = (Math.random() > 0.5) ? new WindowsDialog() : new MacOSDialog();
dialog.render();
The beauty here is that the Dialog.render() method is completely decoupled from the concrete button classes. Adding a LinuxDialog tomorrow is trivial. You’ve successfully separated the invocation (render) from the creation (createButton).
The Abstract Factory: The Whole Damn Furniture Store
If the Factory Method is a personal shopper for one product, the Abstract Factory is a contract for an entire furniture store. You don’t just get a chair; you get a chair, a sofa, and a coffee table that are all guaranteed to be from the same stylistic family (e.g., Modern, Victorian, IKEA).
An Abstract Factory declares interfaces for creating families of related or dependent objects without specifying their concrete classes. You’re not just creating a button; you’re creating a whole suite of UI controls that are guaranteed to look consistent.
// Abstract Products
interface Button { render(): void; }
interface Checkbox { toggle(): void; }
// Concrete Products for Family A
class WindowsButton implements Button { render() { console.log("Windows Button"); } }
class WindowsCheckbox implements Checkbox { toggle() { console.log("Windows Checkbox toggled"); } }
// Concrete Products for Family B
class MacOSButton implements Button { render() { console.log("MacOS Button"); } }
class MacOSCheckbox implements Checkbox { toggle() { console.log("MacOS Checkbox toggled"); } }
// The Abstract Factory Interface: The contract for the entire store.
interface GUIFactory {
createButton(): Button;
createCheckbox(): Checkbox;
}
// Concrete Factories: The actual stores that fulfill the contract.
class WindowsFactory implements GUIFactory {
createButton(): Button { return new WindowsButton(); }
createCheckbox(): Checkbox { return new WindowsCheckbox(); }
}
class MacOSFactory implements GUIFactory {
createButton(): Button { return new MacOSButton(); }
createCheckbox(): Checkbox { return new MacOSCheckbox(); }
}
// Client code. It's blissfully unaware of the concrete classes.
// It just knows it has a factory that can create a matching set.
class Application {
private factory: GUIFactory;
private button: Button | null = null;
constructor(factory: GUIFactory) {
this.factory = factory;
}
createUI() {
this.button = this.factory.createButton();
const checkbox = this.factory.createCheckbox(); // This will be from the same family!
}
render() {
this.button?.render();
}
}
// Application initialization: The one place where the concrete factory is chosen.
const os = "windows"; // determined by some logic
const factory: GUIFactory = os === "windows" ? new WindowsFactory() : new MacOSFactory();
const app = new Application(factory);
app.createUI();
app.render();
The pitfall? It’s a sledgehammer. If your families only have one product, you’ve over-engineered it with an Abstract Factory. Use it when you genuinely have multiple interrelated families to create.
The Builder Pattern: Because Your Constructor Is a Nightmare
You’ve seen the class. The one with 17 constructor parameters, half of which are optional. Trying to instantiate it is an exercise in guesswork and despair. new Something(true, undefined, 42, null, "hello") — what does any of that even mean? The Builder pattern is our savior here. It lets you construct complex objects step by step, allowing you to use different configurations without your constructor looking like a disaster zone.
It separates the construction of a complex object from its representation so that the same construction process can create different representations. It’s like ordering a complex coffee. You don’t yell ingredients at the barista; you specify them step-by-step (“double shot, oat milk, not too hot”).
class Pizza {
public size: number;
public cheese: boolean = false;
public pepperoni: boolean = false;
public olives: boolean = false;
// The constructor is now private! You MUST use the Builder.
private constructor(builder: PizzaBuilder) {
this.size = builder.size;
this.cheese = builder.cheese;
this.pepperoni = builder.pepperoni;
this.olives = builder.olives;
}
// The static Builder method is a common convention to kick things off.
static builder(size: number): PizzaBuilder {
return new PizzaBuilder(size);
}
}
// The Builder class
class PizzaBuilder {
// Required parameters stay in the builder's constructor.
constructor(public size: number) {}
// Optional parameters with sensible defaults.
public cheese: boolean = false;
public pepperoni: boolean = false;
public olives: boolean = false;
// Fluent methods for configuration. Each returns the builder for chaining.
addCheese(): this {
this.cheese = true;
return this;
}
addPepperoni(): this {
this.pepperoni = true;
return this;
}
addOlives(): this {
this.olives = true;
return this;
}
// The final build method that constructs the actual object.
build(): Pizza {
// This is the perfect place for validation logic.
if (this.size > 16) {
throw new Error("Pizza size is too large, be reasonable.");
}
return new Pizza(this);
}
}
// Usage: Clean, readable, and impossible to mess up.
const myPizza = Pizza.builder(14) // Start with required size
.addCheese() // Add what you want
.addPepperoni() // ...
.addOlives() // ...
.build(); // Finally, construct it
console.log(myPizza); // A perfectly built Pizza object.
The build() method is your last line of defense. You can enforce invariants and business rules there, ensuring you never get an invalid object. This pattern is an absolute godsend for configuration-heavy objects and creating immutable objects in a readable way. It makes your code self-documenting. new Pizza(14, true, true, true) is a mystery. Pizza.builder(14).addCheese().addPepperoni().addOlives().build() is a story.