THIS EXPLANATION
THE ROOM
CDA·45 Computing, Data & AI 6 MIN · 8 STATIONS

Composition over inheritance

A Socratic walk-through of composition over inheritance — reasoned out one step at a time, not lectured.

abcdefgh
a

The question we started with

THE QUESTION #

Why did the feature invented to let programs reuse code become the one to avoid?

Inheritance was, for a generation, the headline feature of object-oriented programming: write a class, then write another that gets everything the first had and adds a little. Reuse without copying, taught in every first course.

Then the advice inverted. The Gang of Four wrote "favor object composition over class inheritance" in 1994 — inside the book that popularised object-oriented design. Modern languages ship without implementation inheritance at all and nobody misses it much. So what went wrong with a mechanism that does exactly what it advertises?

b

Reasoning it through

REASONING #

Begin with what inheritance actually gives you, because it gives two things at once and they are usually conflated.

The first is subtyping: a promise that instances of the child can be used wherever the parent was expected. The second is implementation reuse: the child gets the parent's code and fields for free. Those are separate ideas, and inheritance welds them together. Most of the trouble follows from that welding.

Take reuse first. What exactly does a subclass inherit? Not just a set of methods — it inherits the parent's internal arrangement, because its methods run inside the parent's object, touch its fields, and call its other methods. So a subclass depends on which internal method the parent happens to call from which other one. Consider a collection whose addAll is implemented by looping over add, and a subclass that counts insertions by overriding both. The count is doubled — not because either class is wrong, but because the subclass depended on an implementation detail the parent never promised. Rewrite addAll to copy the buffer directly, and the count silently changes again. This is the fragile base class problem: the parent cannot safely change its own private choices, because a subclass may have taken them as an interface.

Notice how much stronger that is than ordinary dependency. A caller of a function depends on its signature and behaviour. A subclass depends on its internals. Inheritance is therefore about the tightest coupling a language offers — and it is offered as the beginner's tool for reuse.

Now take subtyping, and ask what the promise actually costs. If a child can stand in for a parent everywhere, then it must honour everything a caller could rely on about the parent — this is Liskov's substitution principle. The uncomfortable consequence is that inheriting is a commitment to every method, including the ones that make no sense for you. The stock example is a read-only document class inheriting save. It can throw, but then a caller holding a perfectly ordinary reference gets a failure the type system said could not happen. The hierarchy typechecks and lies.

Then there is the shape problem. A class hierarchy is a tree, so every class has exactly one parent, and you must choose which single axis of variation the tree encodes. Real requirements have several axes — storage medium, formatting, permissions, locale — and a tree can only express one of them cleanly. Model two and the leaves multiply combinatorially, which is why old hierarchies acquire classes with names like BufferedEncryptedRemoteFileReader.

So what does composition change? It splits the two ideas apart. An object holds another object and delegates to it, so it reuses behaviour without inheriting internals; and it separately declares which interfaces it implements, so subtyping is asserted only where it is true. The axes stop competing, because each collaborator varies independently — a reader with any storage, any encoding, chosen at construction rather than at compile time by picking a leaf of a tree.

One honest qualification. This is a default, not a prohibition. Inheritance remains a good fit where the hierarchy is genuinely a fixed classification that the author controls end to end — abstract syntax tree nodes, algebraic data types, sealed hierarchies of exceptions — because there the base class and all its children change together by design, and the fragility argument mostly evaporates. What went wrong was inheritance used across module boundaries, and used for reuse rather than for classification.

c

The analogy

THE ANALOGY #
THE FIGURE

Think of hiring a specialist versus being born into a family trade. Hiring a plumber gets you plumbing while leaving you free to hire a different one tomorrow; you depend only on what they agree to do. Inheriting the family trade gets you the workshop, the tools, the habits and the debts — and if the family changes how it does something, your work changes underneath you, because you were never separate from it.

WHERE IT BREAKS DOWN

a tradesperson's contract is negotiated and written, whereas a parent class's real contract is usually undocumented — the subclass discovers what it depended on only when a refactor breaks it, which is worse than any hiring arrangement.

d

Clarifying the model

THE MODEL #

Three clarifications.

First, "composition over inheritance" is not "interfaces are bad". Interface inheritance — declaring that you implement a set of operations — carries none of the fragility, because there is no implementation to depend on. The advice targets implementation inheritance specifically. Languages that kept the first and dropped the second, and use embedding or traits for reuse, are following exactly this reading.

Second, composition is not free. Delegation means writing forwarding methods, more objects to wire up, and indirection at the point where you would like to read the behaviour in one place. Some languages take that sting away with delegation syntax or mixins; in others it is real boilerplate, and pretending otherwise is why the advice sometimes feels doctrinaire.

Third, the useful test at the moment of choice is not "is this an X" but "would I accept being broken by this parent's private changes, forever?" If the parent lives in another library, another team, or another release cadence, the answer is almost always no — and then composition is not a stylistic preference but the only arrangement that survives the next upgrade.

e

A picture of it

THE PICTURE #
Composition over inheritance
Composition over inheritance The upper pair is the inheritance arrangement: the solid triangle arrow means the child receives everything, including a save it must refuse at run time, so the type says more than the object can deliver. The lower group is the composed alternative -- the dotted triangles are interface implementation, which promises only a signature, and the open diamond is the editor holding a store rather than being one. Read-only is now a property of the collaborator you pass in, not a leaf of a tree, so adding a second axis of variation adds a second collaborator instead of multiplying classes. {"generator":"[email protected]","source":"../Socrates/.diagram-cache/_src/composition-over-inheritance.md","sourceIndex":1,"sourceLine":4,"sourceHash":"5dd5e53fa90f008cf287a8f920f58f63b7695c3cc871be522838c92307a7e901","diagramType":"class","layoutVariant":"source","repairedDuplicateIds":[],"motion":"entrance-with-reduced-motion-fallback","presentation":"editorial","attempt":1,"viewBox":{"x":0,"y":0,"width":720,"height":707},"qa":{"passed":true,"findings":[]}} inherits a promise itcannot keep holds one, chosen whenbuilt Document +render() +save() ReadOnlyDocument +save() : throws Editor +render() +save() Store +write() FileStore +write() ReadOnlyStore +write() : rejected at construction

How to readThe upper pair is the inheritance arrangement: the solid triangle arrow means the child receives everything, including a save it must refuse at run time, so the type says more than the object can deliver. The lower group is the composed alternative — the dotted triangles are interface implementation, which promises only a signature, and the open diamond is the editor holding a store rather than being one. Read-only is now a property of the collaborator you pass in, not a leaf of a tree, so adding a second axis of variation adds a second collaborator instead of multiplying classes.

f

What became clearer

WHAT CLEARED #
WHAT CLEARED

Inheritance bundles two things: the claim that one type substitutes for another, and the borrowing of another class's implementation. The claim is often fine; the borrowing binds you to internals the parent never promised to keep, which makes the parent unable to evolve and the child unable to trust it. Composition separates them, so reuse costs only a documented interface and substitutability is asserted only where it holds. The reversal was never about the feature being broken — it was about a mechanism with the tightest coupling in the language having been taught as the first tool for the loosest possible purpose.

g

Where to go next

ONWARD #
  • How mixins, traits, and embedding try to recover inheritance's convenience without its fragility.
  • Liskov substitution as a rule about behaviour rather than signatures, and what it forbids in practice.
h

Key terms

TERMS #
TermWhat it means
Implementation inheritanceacquiring a parent class's code and fields, as opposed to merely declaring conformance to its interface.
Fragile base class problemthe effect where an internal change in a parent class silently breaks subclasses that depended on its internal call structure.
Liskov substitution principlethe requirement that an instance of a subtype be usable anywhere the supertype is expected, without weakening what callers may rely on.
Delegationforwarding work to a held object, the mechanism by which composition achieves reuse.

Every term the collection defines is gathered in the glossary.

Nearby on the shelf

4