Abstract Factory
Produce families of related objects without specifying their concrete classes. When your system needs multiple objects that belong together, Abstract Factory enforces that they always come from the same family.
The Problem
You’re building a cross-platform UI library. You need Button, Checkbox, and Dialog components — but the exact look depends on the OS: macOS, Windows, or Linux. The wrong approach is scattering if (os === 'mac') checks everywhere. Worse, nothing prevents you from accidentally pairing a macOS Button with a Windows Checkbox — a combination that looks broken.
// Bad: scattered ifs, mismatched families possible
const btn = os === 'mac' ? new MacButton() : new WinButton();
const cb = os === 'mac' ? new MacCheckbox() : new LinuxCheckbox(); // oops, mixed!
You need a way to guarantee that all UI widgets come from the same family.
The Solution
Define a factory interface with one method per product type. Each concrete factory creates an entire family of matching products. Client code talks only to the factory interface — it never instantiates a concrete class directly.
Real-World Analogy
A furniture manufacturer produces matching sets: Victorian sofa + Victorian chair + Victorian table. An Art Deco manufacturer produces a different matching set. You order from one manufacturer to guarantee your furniture matches — you don’t mix-and-match pieces from different catalogues.
TypeScript Example
// Abstract products
interface Button {
render(): string;
onClick(handler: () => void): void;
}
interface Checkbox {
render(): string;
toggle(): void;
}
// Concrete products — macOS family
class MacButton implements Button {
render() { return '<MacButton style="rounded" />'; }
onClick(handler: () => void) { handler(); }
}
class MacCheckbox implements Checkbox {
private checked = false;
render() { return `<MacCheckbox checked=${this.checked} />`; }
toggle() { this.checked = !this.checked; }
}
// Concrete products — Windows family
class WinButton implements Button {
render() { return '<WinButton style="flat" />'; }
onClick(handler: () => void) { handler(); }
}
class WinCheckbox implements Checkbox {
private checked = false;
render() { return `<WinCheckbox checked=${this.checked} />`; }
toggle() { this.checked = !this.checked; }
}
// Abstract factory
interface GUIFactory {
createButton(): Button;
createCheckbox(): Checkbox;
}
// Concrete factories — each produces a matching family
class MacFactory implements GUIFactory {
createButton(): Button { return new MacButton(); }
createCheckbox(): Checkbox { return new MacCheckbox(); }
}
class WinFactory implements GUIFactory {
createButton(): Button { return new WinButton(); }
createCheckbox(): Checkbox { return new WinCheckbox(); }
}
// Client code — only depends on abstract interfaces
class Application {
private button: Button;
private checkbox: Checkbox;
constructor(factory: GUIFactory) {
// Always a matched family — no accidental mixing
this.button = factory.createButton();
this.checkbox = factory.createCheckbox();
}
render() {
return [this.button.render(), this.checkbox.render()].join('\n');
}
}
// Configuration drives which factory to use
function createFactory(os: 'mac' | 'win'): GUIFactory {
return os === 'mac' ? new MacFactory() : new WinFactory();
}
const factory = createFactory('mac');
const app = new Application(factory);
console.log(app.render());
// → <MacButton style="rounded" />
// → <MacCheckbox checked=false />
When to Use
- Your system needs to work with families of related objects and you want to enforce consistency within a family
- You have multiple product variants and want to swap between them without rewriting client code
- You’re building a library and want to expose creation points for users to extend
Pros
- Guarantees that products from a factory are always compatible with each other
- Avoids tight coupling between client code and concrete product classes
- Follows the Single Responsibility Principle — creation logic lives in one place per family
- Follows the Open/Closed Principle — add new families without touching existing code
Cons
- Adding a new product type (e.g.
Dialog) requires updating every factory — can be disruptive - Can over-engineer simple scenarios where only one variant exists
Related Patterns
- Factory Method is the per-method building block that Abstract Factory uses internally
- Prototype can replace Abstract Factory when product variants are better expressed as clones
- Builder focuses on step-by-step construction; Abstract Factory focuses on families