There are two ways to share behaviour between objects.
- Inheritance (“is a”):
Dog extends Animal. - Composition (“has a” / “uses a”): an object contains other objects or functions and delegates work to them.
The problem with deep inheritance
Imagine game characters:
Character
├── Fighter
│ └── MagicFighter ← a fighter who also casts spells
└── Wizard
└── ... ← a wizard who also fights?
Soon you need something that does both, and the tree does not fit. Changes to a parent class also ripple into every child, which makes the code fragile.
Composition: assemble from small parts
Instead of asking “what is it?”, ask “what can it do?” and give each ability its own small piece.
const canWalk = state => ({
walk() { return `${state.name} walks`; }
});
const canSwim = state => ({
swim() { return `${state.name} swims`; }
});
const canFly = state => ({
fly() { return `${state.name} flies`; }
});
function createDuck(name) {
const state = { name };
return { ...canWalk(state), ...canSwim(state), ...canFly(state) };
}
function createPenguin(name) {
const state = { name };
return { ...canWalk(state), ...canSwim(state) }; // no flying
}
const duck = createDuck("Duck");
console.log(duck.fly()); // → Duck flies
console.log(typeof createPenguin("Pingu").fly); // → undefined
Each ability is independent. A new combination is a new line, not a new class in a tree.
Composition with classes
An object can hold other objects.
class Engine {
start() { return "Engine started"; }
}
class Radio {
play() { return "Music playing"; }
}
class Car {
engine = new Engine();
radio = new Radio();
drive() {
return `${this.engine.start()}, driving. ${this.radio.play()}`;
}
}
console.log(new Car().drive()); // → Engine started, driving. Music playing
A Car has an Engine. It is not an engine.
Another practical case: components
User interface code is full of composition: a page contains a header, a list and a footer, and each is its own piece.
How to choose
| Situation | Prefer |
|---|---|
| A true “is a” relationship, a small stable hierarchy | inheritance |
| Behaviour mixes in many combinations | composition |
| You want to swap parts (different loggers, storage) | composition |
| You are unsure | start with composition |
A useful saying
> “Favor composition over inheritance.”
It is a guideline, not a rule. Inheritance is fine in small, clear cases (such as custom errors). The danger is deep, wide hierarchies built “just in case”.
Dependency injection
Composition makes testing easier: pass the parts in.
class Notifier {
constructor(sender) {
this.sender = sender; // any object with a send() method
}
notify(message) {
return this.sender.send(message);
}
}
const emailSender = { send: msg => `email: ${msg}` };
const smsSender = { send: msg => `sms: ${msg}` };
console.log(new Notifier(emailSender).notify("Hi")); // → email: Hi
console.log(new Notifier(smsSender).notify("Hi")); // → sms: Hi
The Notifier does not care how messages are sent. That flexibility is the main benefit.