7.3 KiB
id, title, category, priority, tags, related
| id | title | category | priority | tags | related | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| core-composition | Composition Over Inheritance | core-principles | critical |
|
|
Composition Over Inheritance
Favor composing objects from smaller, focused pieces over building deep inheritance hierarchies. Composition provides more flexibility, better encapsulation, and avoids the fragile base class problem.
Bad Example
// Anti-pattern: Deep inheritance hierarchy
class Animal {
protected name: string;
protected energy: number = 100;
constructor(name: string) {
this.name = name;
}
eat(amount: number): void {
this.energy += amount;
console.log(`${this.name} is eating. Energy: ${this.energy}`);
}
sleep(hours: number): void {
this.energy += hours * 10;
console.log(`${this.name} slept for ${hours} hours. Energy: ${this.energy}`);
}
}
class Bird extends Animal {
fly(): void {
this.energy -= 20;
console.log(`${this.name} is flying. Energy: ${this.energy}`);
}
}
class Duck extends Bird {
swim(): void {
this.energy -= 5;
console.log(`${this.name} is swimming. Energy: ${this.energy}`);
}
quack(): void {
console.log(`${this.name} says quack!`);
}
}
class FlyingFish extends Animal {
// Problem: Can't inherit from both Bird and Fish
// Must duplicate flying code or create awkward hierarchy
swim(): void {
this.energy -= 5;
console.log(`${this.name} is swimming. Energy: ${this.energy}`);
}
// Duplicated from Bird class!
fly(): void {
this.energy -= 20;
console.log(`${this.name} is flying. Energy: ${this.energy}`);
}
}
class Penguin extends Bird {
// Problem: Penguins can't fly but inherit fly()
// Must override to throw error - LSP violation
fly(): void {
throw new Error('Penguins cannot fly!');
}
swim(): void {
this.energy -= 5;
console.log(`${this.name} is swimming. Energy: ${this.energy}`);
}
}
// More problems:
// - What about a robot bird? It doesn't eat or sleep.
// - What about a bat? It flies but isn't a bird.
// - Every change to Animal affects all subclasses.
// - Testing requires understanding entire hierarchy.
Good Example
// Correct approach: Composition with focused behaviors
// Define behaviors as interfaces
interface Eater {
eat(amount: number): void;
}
interface Sleeper {
sleep(hours: number): void;
}
interface Flyer {
fly(): void;
}
interface Swimmer {
swim(): void;
}
interface Speaker {
speak(): void;
}
// Implement behaviors as standalone classes
class StandardEater implements Eater {
constructor(private entity: { name: string; energy: number }) {}
eat(amount: number): void {
this.entity.energy += amount;
console.log(`${this.entity.name} is eating. Energy: ${this.entity.energy}`);
}
}
class StandardSleeper implements Sleeper {
constructor(private entity: { name: string; energy: number }) {}
sleep(hours: number): void {
this.entity.energy += hours * 10;
console.log(`${this.entity.name} slept for ${hours} hours. Energy: ${this.entity.energy}`);
}
}
class WingedFlyer implements Flyer {
constructor(
private entity: { name: string; energy: number },
private energyCost: number = 20
) {}
fly(): void {
this.entity.energy -= this.energyCost;
console.log(`${this.entity.name} is flying. Energy: ${this.entity.energy}`);
}
}
class AquaticSwimmer implements Swimmer {
constructor(
private entity: { name: string; energy: number },
private energyCost: number = 5
) {}
swim(): void {
this.entity.energy -= this.energyCost;
console.log(`${this.entity.name} is swimming. Energy: ${this.entity.energy}`);
}
}
// Compose animals from behaviors
class Duck implements Eater, Sleeper, Flyer, Swimmer, Speaker {
public name: string;
public energy: number = 100;
private eater: Eater;
private sleeper: Sleeper;
private flyer: Flyer;
private swimmer: Swimmer;
constructor(name: string) {
this.name = name;
this.eater = new StandardEater(this);
this.sleeper = new StandardSleeper(this);
this.flyer = new WingedFlyer(this);
this.swimmer = new AquaticSwimmer(this);
}
eat(amount: number): void {
this.eater.eat(amount);
}
sleep(hours: number): void {
this.sleeper.sleep(hours);
}
fly(): void {
this.flyer.fly();
}
swim(): void {
this.swimmer.swim();
}
speak(): void {
console.log(`${this.name} says quack!`);
}
}
// Penguin: swims but doesn't fly - no problem!
class Penguin implements Eater, Sleeper, Swimmer, Speaker {
public name: string;
public energy: number = 100;
private eater: Eater;
private sleeper: Sleeper;
private swimmer: Swimmer;
constructor(name: string) {
this.name = name;
this.eater = new StandardEater(this);
this.sleeper = new StandardSleeper(this);
this.swimmer = new AquaticSwimmer(this);
}
eat(amount: number): void {
this.eater.eat(amount);
}
sleep(hours: number): void {
this.sleeper.sleep(hours);
}
swim(): void {
this.swimmer.swim();
}
speak(): void {
console.log(`${this.name} says squawk!`);
}
}
// Flying fish: swims and flies - easy!
class FlyingFish implements Swimmer, Flyer {
public name: string;
public energy: number = 100;
private swimmer: Swimmer;
private flyer: Flyer;
constructor(name: string) {
this.name = name;
this.swimmer = new AquaticSwimmer(this);
this.flyer = new WingedFlyer(this, 30); // Different energy cost
}
swim(): void {
this.swimmer.swim();
}
fly(): void {
this.flyer.fly();
}
}
// Robot bird: flies but doesn't eat or sleep
class RobotBird implements Flyer, Speaker {
public name: string;
public energy: number = 100;
private flyer: Flyer;
constructor(name: string) {
this.name = name;
this.flyer = new WingedFlyer(this, 10); // Efficient robot
}
fly(): void {
this.flyer.fly();
}
speak(): void {
console.log(`${this.name} says BEEP BOOP!`);
}
recharge(): void {
this.energy = 100;
console.log(`${this.name} recharged to full energy.`);
}
}
// Functions work with any entity that has the required behavior
function makeEntityFly(flyer: Flyer): void {
flyer.fly();
}
function feedEntity(eater: Eater, amount: number): void {
eater.eat(amount);
}
// Works with duck, flying fish, or robot bird
makeEntityFly(new Duck('Donald'));
makeEntityFly(new FlyingFish('Nemo'));
makeEntityFly(new RobotBird('R2D2'));
// Works with duck or penguin, but not robot bird (correctly!)
feedEntity(new Duck('Donald'), 50);
feedEntity(new Penguin('Pingu'), 50);
// feedEntity(new RobotBird('R2D2'), 50); // Type error - RobotBird isn't an Eater
Why
-
Flexibility: Compose any combination of behaviors. No artificial hierarchy constraints.
-
Avoids Diamond Problem: No multiple inheritance issues. Just implement multiple interfaces.
-
LSP Compliance: No need to override methods to throw errors. Types only have methods they actually support.
-
Reusability: Behaviors can be reused across unrelated types.
-
Testability: Test behaviors in isolation. Mock specific behaviors easily.
-
Runtime Flexibility: Can change behaviors at runtime by swapping implementations.
-
Stable Dependencies: Behavior implementations are stable. Adding new composed types doesn't affect existing code.