Topic 7.8
Composition over Inheritance
In one line
Instead of building tall family trees of classes, build objects from small parts: an object has an engine rather than is an engine, borrows abilities through mixins, or hands work to a helper (delegation); and when there is no shared behaviour to organise, a plain object and a few functions beat a class.
Think of it like this
Real-life example: LEGO versus a carved statue. A carved wooden statue of a horse is one solid piece: if you want a horse with wings, you must carve a new statue. A LEGO horse is made of parts; to add wings you clip on two wing bricks, and the same wings fit a LEGO pig. Inheritance is carving: each new kind needs a new class in the tree. Composition is LEGO: you build objects from small parts and swap them freely.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Composition
- Building an object from smaller parts it holds and uses, instead of inheriting from a parent class.
- has-a relation
- One thing contains or uses another: a car has an engine. Fits composition.
- Delegation
- An object passing a job to a helper object it holds, like
car.start()callingengine.start(). - Duck typing
- Treating any object as fitting if it has the methods you need: if it quacks like a duck, use it as a duck.
- Mixin
- A reusable bundle of methods added to classes or objects that need it.
- Subclass factory
- A function that takes a class and returns a new class extending it with extra methods.
- Factory function
- A normal function that builds and returns a new object, without
classornew. - Fragile base class
- When changing a parent class breaks child classes that depended on its details.
Step by step
01Watch an inheritance tree crack
Birds fly, so Bird gets fly(). Then a penguin arrives, and a duck that also swims, and a fish that flies. Each new animal forces you to reshape the tree.
02Build abilities as small parts
Write each ability once, as a function that returns an object with one method. The state object is shared by the parts of one animal.
const canFly = (state) => ({ fly: () => state.name + " flies" });
const canSwim = (state) => ({ swim: () => state.name + " swims" });03Assemble each animal from the parts it needs
A duck takes flying and swimming; a penguin only swimming. No tree, no compromises: a flying fish is just canFly plus canSwim.
function createDuck(name) {
const state = { name };
return { name, ...canFly(state), ...canSwim(state) };
}
function createPenguin(name) {
const state = { name };
return { name, ...canSwim(state) };
}04Delegate to a part you can swap
A car holds an engine and forwards start() to it. Any object with a start() method fits, so the engine can change while the program runs.
05Pick the simplest tool
Ask in order: is this just data? Use a plain object. Only one of it? An object literal or module. Many instances sharing behaviour? A class or factory. Shared abilities across unrelated classes? Composition or a mixin. A true, shallow is-a? extends.
Try it yourself
- 1
Invent a new animal
In the first example, write
createSeagull(name)with flying, swimming and a new abilitycanScreech. Add it to the zoo and predict the new list item. - 2
Break the duck typing
In the second example, swap in
{ go: () => "vroom" }instead of the toy motor. Predict the error message before running. - 3
Clash two mixins
In the third example, add a
touch()method toWithEventsthat returns"events touch". WhichtouchdoesUserget now? Predict, then logu.touch(new Date(0)).
Code & diagrams
No class tree at all. The penguin simply never gets `fly`, and the flying fish combines two parts that a single-parent tree couldn't.
Preview
Console
Nothing logged yet.Expected console output
Donald flies | Donald says quack
Pingu swims | can fly? false
Zip flies | Zip swimsThe car **has** an engine; it isn't one. Swapping the part at runtime is something inheritance can't do.
Preview
Console
Nothing logged yet.Expected console output
Swift: petrol engine (90 hp) roars
Swift: electric motor hums
car is an engine? false
Swift: toy motor whirrsEach mixin is a function from a class to a new subclass, so they stack: `User` → events → timestamps → `Model`. `??=` (ES2021) creates the list the first time.
Preview
Console
Nothing logged yet.Expected console output
changed to Asha K
User { name: 'Asha K', updatedAt: '2024-01-15T10:30:00.000Z' }
true function functionThe destructured `increment` still works because it uses a closure, not `this`. `Object.assign` copies mixin methods as enumerable keys, unlike real class methods.
Preview
Console
Nothing logged yet.Expected console output
dark Rs 5.00 12
count is hidden: [ 'increment', 'value' ]
{"x":1,"y":2} [ 'toText' ]Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Two Object.assign mixins with the same method
Mix logger = { log() { return "logger"; } } and auditor = { log() { return "auditor"; } } into the same prototype with Object.assign(Thing.prototype, logger, auditor).
Break #2
Delegate to a part that lacks the method
Give the car { go: () => "vroom" } as its engine and call car.start().
Myth vs fact
Myth
Composition means never using inheritance.
Fact
It means preferring it by default. Inheritance is still right for real, shallow is-a relations like custom errors or custom elements.
Myth
Classes are the professional way; functions are for beginners.
Fact
Many large codebases (and frameworks like React with hooks) are built mostly from functions and plain objects. The right choice depends on the problem, not on seniority.
Myth
Mixins are the same as multiple inheritance.
Fact
They give a similar effect, but each mixin is applied one after another in a single chain (subclass factories) or copied flat (Object.assign). The order decides who wins a clash.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Subclass-factory mixins create a new class per application:
WithEvents(Model)called twice gives two different classes, so cache the result if you compare classes or rely oninstanceofof the mixin. Each anonymous mixin class also appears in the prototype chain, making it longer. - ▸
Object.assigninvokes getters on the source and copies their current values, not the accessors. To copy getters and setters as accessors, useObject.defineProperties(target, Object.getOwnPropertyDescriptors(source)). Methods usingsuperkeep their original home object when copied (Topic 7.7). - ▸
Factory functions with closures allocate one function per method per object. With hundreds of objects that's irrelevant; with millions (game entities, big data grids) prototype-based classes are measurably lighter. Engines also optimise class instances with stable shapes well.
- ▸
The strategy pattern is composition in action: pass behaviour in as an object or function (
new Sorter(compareByPrice)) instead of making a subclass per behaviour. Topic 15.2 and Hectal's/system-designcourse cover it with the other design patterns.
Remember this
- 1
Composition means building an object out of other objects or functions that each do one job, and having the object use them. It models a "has-a" relation: a car has an engine; a user has an address. Inheritance (Topic 7.7) models "is-a". The saying "favour composition over inheritance" (from the 1994 book *Design Patterns*) means: reach for has-a first, and use is-a only when it's really true and stays simple.
- 2
Inheritance trees break in predictable ways. Rigid categories:
Birdhasfly(), then you need aPenguin. The diamond: aFlyingFishwants bothFishandBird, but a class can only extend one parent. Fragile base class: a change in a parent silently breaks children deep below. The gorilla-banana problem: you wanted a banana (one method) but got the gorilla holding it and the whole jungle (everything the parent drags along). - 3
Delegation is composition's basic move: an object keeps a helper in a field and passes work to it.
car.start()callsthis.#engine.start(). The car doesn't care what kind of engine it holds, as long as it has astart()method; this "if it has the right methods, it fits" idea is called duck typing. You can even swap the part at runtime (car.swapEngine(new ElectricMotor())), which no class hierarchy allows. Prototype links (Topic 7.3) are a built-in form of delegation too. - 4
A mixin is a reusable bundle of methods mixed into other classes or objects. Two common forms: copying methods onto a prototype with
Object.assign(Point.prototype, serializable), and subclass factories, functions that take a class and return a new class extending it:const WithEvents = (Base) => class extends Base { on() {...} emit() {...} };thenclass User extends WithEvents(Model) {}. Factories keepsuperandinstanceof Modelworking. Watch out for name clashes (two mixins defining the same method silently overwrite each other) and hidden dependencies between mixins. - 5
Factory functions create objects without
classornew:function createCounter() { let count = 0; return { increment() { count++; }, get value() { return count; } }; }. Data hidden in the closure is private, and methods don't usethis, so they can never be detached and lose it. The cost is one copy of each method per object, which only matters with thousands of objects. Building an object from several small factories withObject.assignor spread is called functional composition of objects. - 6
When is a class overkill? When you only need data (use a plain object or JSON), when you'd create only one instance (use an object literal or a module), or when the "class" would hold only static helpers (use plain functions in a module, Phase 12). Classes fit when you create many objects that share behaviour, need
#privatestate with shared methods, extend built-ins likeError, or a platform requires them (custom elements). Most real programs mix all three styles.
Explain it without notes
What does "favour composition over inheritance" mean, and why is it good advice?
What is delegation, and how does duck typing make it flexible?
What are mixins in JavaScript? Compare Object.assign mixins with subclass factories.
When is a class overkill, and when is it the right tool?
Practice
Write factories withName(name) and withGreeting(state) and combine them into createPerson("Asha") so that person.greet() returns "Hello, I am Asha", without class or this.
Write a class Logger that has a writer (any object with write(text)), and a method info(msg) that calls writer.write("[info] " + msg). Use a writer that pushes into an array, and print the array after two calls.
Write a subclass-factory mixin WithCount that adds a count field and an increment() method, apply it to an empty class Base {}, and show two instances counting separately.
Trade-offs
- ↔
Composition gives flexibility and small testable parts, but spreads behaviour over several objects, so you may need to look in more places to see what an object can do.
- ↔
Factory functions avoid
thisand give true privacy, but use more memory per object and don't work withinstanceof; classes share methods and have clear types, but bringthispitfalls. - ↔
Mixins share code between unrelated classes, but clashes and hidden dependencies grow with each one; past two or three, prefer explicit helper fields.
Done when you can
I can tell an is-a relation from a has-a relation.
I can build objects from small ability factories.
I can delegate work to a swappable helper using duck typing.
I can write a subclass-factory mixin and an
Object.assignmixin, and know their pitfalls.I can decide when a plain object, a factory function or a class fits best.