Command Palette

Search for a command to run...

Hectal
PHASE 7Intermediate ~29 min· topic 8 of 8

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() calling engine.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 class or new.
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.

Watch an inheritance tree crackdiagram
Rendering diagram…

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.

abilities.jswhole filejavascript
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.

abilities.jswhole filejavascript
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.

Delegate to a part you can swapdiagram
Rendering diagram…

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. 1

    Invent a new animal

    In the first example, write createSeagull(name) with flying, swimming and a new ability canScreech. Add it to the zoo and predict the new list item.

  2. 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. 3

    Clash two mixins

    In the third example, add a touch() method to WithEvents that returns "events touch". Which touch does User get now? Predict, then log u.touch(new Date(0)).

Code & diagrams

Animals built from abilities New tab

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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Donald flies | Donald says quack
Pingu swims | can fly? false
Zip flies | Zip swims
Delegation: a car that has an engine New tab

The car **has** an engine; it isn't one. Swapping the part at runtime is something inheritance can't do.

Preview

Press Run to see the page.

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 whirrs
Mixins as subclass factories ES2022+ New tab

Each 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

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

changed to Asha K
User { name: 'Asha K', updatedAt: '2024-01-15T10:30:00.000Z' }
true function function
When a class is overkill New tab

The destructured `increment` still works because it uses a closure, not `this`. `Object.assign` copies mixin methods as enumerable keys, unlike real class methods.

Preview

Press Run to see the page.

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).

terminal
$ Console
── what you'll see ──
new Thing().log() returns "auditor" and the logger's method is silently gone.

Break #2

Delegate to a part that lacks the method

Give the car { go: () => "vroom" } as its engine and call car.start().

terminal
$ Console
── what you'll see ──
Uncaught TypeError: this.#engine.start is not a function

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 on instanceof of the mixin. Each anonymous mixin class also appears in the prototype chain, making it longer.

  • ▸

    Object.assign invokes getters on the source and copies their current values, not the accessors. To copy getters and setters as accessors, use Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)). Methods using super keep 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-design course cover it with the other design patterns.

Remember this

  1. 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. 2

    Inheritance trees break in predictable ways. Rigid categories: Bird has fly(), then you need a Penguin. The diamond: a FlyingFish wants both Fish and Bird, 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. 3

    Delegation is composition's basic move: an object keeps a helper in a field and passes work to it. car.start() calls this.#engine.start(). The car doesn't care what kind of engine it holds, as long as it has a start() 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. 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() {...} }; then class User extends WithEvents(Model) {}. Factories keep super and instanceof Model working. Watch out for name clashes (two mixins defining the same method silently overwrite each other) and hidden dependencies between mixins.

  5. 5

    Factory functions create objects without class or new: function createCounter() { let count = 0; return { increment() { count++; }, get value() { return count; } }; }. Data hidden in the closure is private, and methods don't use this, 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 with Object.assign or spread is called functional composition of objects.

  6. 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 #private state with shared methods, extend built-ins like Error, or a platform requires them (custom elements). Most real programs mix all three styles.

Explain it without notes

01

What does "favour composition over inheritance" mean, and why is it good advice?

02

What is delegation, and how does duck typing make it flexible?

03

What are mixins in JavaScript? Compare Object.assign mixins with subclass factories.

04

When is a class overkill, and when is it the right tool?

Practice

01

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.

02

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.

03

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 this and give true privacy, but use more memory per object and don't work with instanceof; classes share methods and have clear types, but bring this pitfalls.

  • ↔

    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.assign mixin, and know their pitfalls.

  • I can decide when a plain object, a factory function or a class fits best.