Composition over Inheritance
Introduction#
“Composition over inheritance” is a fundamental design principle that determines whether your code evolves gracefully or becomes unmaintainable. The Gang of Four Design Patterns book advocates building complex objects by combining simpler objects rather than creating inheritance hierarchies. This choice shapes every future change to your system.
The Inheritance Trap#
Inheritance creates a deceptive promise: model the world with hierarchies and get code reuse for free. The reality is different. Consider a payment system:
abstract class Payment {
abstract process(amount: number): boolean;
}
class CreditCardPayment extends Payment {
process(amount: number) { /* credit card logic */ }
}
class LoggedCreditCardPayment extends CreditCardPayment {
process(amount: number) {
console.log(`Processing ${amount}`);
return super.process(amount);
}
}
// Now add fraud detection, retries, notifications...
// Each combination needs a new class
Inheritance forces you into rigid hierarchies where every new requirement creates an explosion of classes. Adding logging and fraud detection to three payment types requires nine classes. The mathematics work against you.
Composition: Linear Scaling#
Composition solves the exponential growth problem by building objects from independent components:
interface PaymentProcessor {
process(amount: number): boolean;
}
interface Logger {
log(message: string): void;
}
interface FraudDetector {
isValid(amount: number): boolean;
}
class PaymentService {
constructor(
private processor: PaymentProcessor,
private logger?: Logger,
private fraudDetector?: FraudDetector
) {}
processPayment(amount: number): boolean {
if (this.fraudDetector && !this.fraudDetector.isValid(amount)) return false;
if (this.logger) this.logger.log(`Processing ${amount}`);
return this.processor.process(amount);
}
}
Three payment types with two optional behaviors still requires only three classes plus the shared components. Composition scales linearly while inheritance scales exponentially.
When to Use Each Approach#
Use Inheritance When:
- You need polymorphism across truly related types
- The hierarchy is shallow (1-2 levels maximum)
- Behavior is identical, not just similar
Use Composition When:
- You need to combine multiple behaviors
- Behavior can change at runtime
- You want to test components independently
- Requirements will evolve (which is always)
The Mathematics of Maintenance#
Inheritance creates exponential complexity. With n base behaviors and m optional features, inheritance requires n × 2^m classes. Composition requires n + m components.
For a payment system with 3 processors (credit, debit, PayPal) and 3 optional features (logging, fraud detection, retry logic):
- Inheritance: 3 × 2³ = 24 classes
- Composition: 3 + 3 = 6 components
This isn’t just about lines of code. Each inheritance class represents a unique combination you must test, debug, and maintain. Composition components can be tested in isolation and combined freely.
Key Insight: Inheritance optimizes for code reuse. Composition optimizes for change. In business software, change is the only constant.
Conclusion#
Choose composition by default. Use inheritance only when you need polymorphism across genuinely identical behaviors. The Gang of Four’s advice remains sound: favor object composition over class inheritance.
Your future self—the one maintaining and extending the code—will thank you.
“Favor object composition over class inheritance” — Gang of Four, Design Patterns