All topics
Objectsadvanced

Composition Over Inheritance

Building complex objects by combining small, focused behaviors together rather than through deep class inheritance hierarchies.

Inheritance says what a thing is. Composition says what it can do. The second scales better.

Inheritance is like building furniture by only ever taking one existing chair and modifying it into slightly different chairs each generation — eventually you're stuck if you need something that's part-chair, part-ladder. Composition is like building with modular LEGO pieces: you snap together exactly the capabilities you need for this specific object, without being constrained by what any single 'parent brick' looked like.

Key Concepts

1
Where inheritance breaks down.
    class Animal { eat() {} }
    class Bird extends Animal { fly() {} }
    class Penguin extends Bird { }      // a bird that cannot fly
2
Now Penguin inherits fly. The options are to override it to throw, restructure the whole hierarchy, or accept a lie.
Penguinfly
3
That is the general failure: a hierarchy encodes one classification, and real requirements cut across it. Adding Duck, which swims and flies, forces a choice about which parent owns swimming.
Duck
4
Composition builds from capabilities.
    const canEat  = (s) => ({ eat:  () => `${s.name} eats` });
    const canFly  = (s) => ({ fly:  () => `${s.name} flies` });
    const canSwim = (s) => ({ swim: () => `${s.name} swims` });
${s.name} eats${s.name} flies${s.name} swims
5
    function createDuck(name) {
      const state = { name };
      return { ...canEat(state), ...canFly(state), ...canSwim(state) };
    }
6
    function createPenguin(name) {
      const state = { name };
      return { ...canEat(state), ...canSwim(state) };   // no fly
    }
7
A penguin does not have fly because it was never given it. Nothing is overridden and nothing lies.
fly
8
The plainer version, which is usually what you want.
    class Order {
      constructor(validator, calculator, notifier) {
        this.validator = validator;
        this.calculator = calculator;
        this.notifier = notifier;
      }
    }
9
Dependencies are passed in rather than inherited. Each part is testable alone and replaceable — which is the practical benefit, more than the taxonomy argument.
10
React's history is the clearest example. Mixins came first and had name collisions and unclear dependencies. Higher-order components followed, and nested awkwardly. Hooks are composition: a component calls the behaviours it needs.
11
    function Profile() {
      const user = useUser();
      const theme = useTheme();
      const online = useOnlineStatus();
    }
12
When inheritance is right. A genuine is-a relationship with shared implementation that will not need to vary independently — HttpError extends Error, or framework base classes.
HttpError extends Error
13
The signals to stop. Overriding a method to disable it. A class hierarchy more than two or three deep. A base class that grows whenever a subclass needs something. Each means the hierarchy is being bent to fit.
14
What the interviewer is probing.1. "How do you compose behaviour in JavaScript?" Probing: the techniques. Stalls: "With classes." Moves up: object composition and higher-order functions — assigning behaviours onto an object, or wrapping a function to add one.
15
2. "What is wrong with deep class hierarchies here?" Probing: the same fragility as elsewhere. Stalls: "Nothing." Moves up: subclasses depend on the parent's implementation and JavaScript gives you no compile-time check, so breakage shows up at runtime.
16
3. "What is a mixin?" Probing: the pattern. Stalls: "A base class." Moves up: a set of methods copied or assigned onto a target, letting an object gain capabilities from several sources.
17
4. "When is a class still appropriate?" Probing: balance. Stalls: "Never." Moves up: when there is genuine shared state and a stable is-a relationship, or when a framework expects one.