Skip to main content

Conceptintermediate

Composition vs. Inheritance

Overview

Inheritance and composition are two ways of reusing behaviour. The classic advice — prefer composition over inheritance — is good and is frequently applied without the criterion that makes it useful.

The criterion: inheritance declares that one type is substitutable for another. Composition declares that one type uses another. If the substitutability is not true, inheritance is wrong regardless of how much code it saves.

Problem

Inheritance is attractive because it saves typing. A subclass gets everything from the superclass for free, and the saving is immediate and visible.

The cost is neither immediate nor visible, and it comes in three forms.

Total coupling. The subclass depends on the superclass's implementation, not just its interface. Internal changes in the superclass break subclasses — what Gamma et al. call breaking encapsulation between classes.

Axis rigidity. A hierarchy commits to one axis of variation. If a second axis appears later, the hierarchy explodes combinatorially or becomes a parallel hierarchy.

Silent violation of substitutability. A subclass that restricts what the superclass allows compiles, passes the tests, and breaks in production when it arrives through an unforeseen path. It is the L of SOLID.

Core Concepts

The substitutability test

Before inheriting, answer: everywhere the superclass is accepted, does the subclass work without the caller knowing the difference?

If the answer has exceptions, inheritance is wrong. It is not a matter of style.

The classic example: Square inheriting from Rectangle. Mathematically a square is a rectangle. In code, rectangle.setWidth(5) followed by rectangle.setHeight(3) produces area 15 for a rectangle and surprising behaviour for a square. The mathematical relation does not survive mutability.

Composition solves multiple axes

Inheritance ties you to one axis. Composition lets you combine.

inheritance — 2 axes, 6 classes composition — 2 axes, 5 pieces
────────────────────────────── ──────────────────────────────
UrgentEmailNotifier Notifier(channel, priority)
NormalEmailNotifier channels: Email, SMS, Push
UrgentSmsNotifier priorities: Urgent, Normal
NormalSmsNotifier
UrgentPushNotifier
NormalPushNotifier

With three axes, the hierarchy would have eighteen classes; the composition, eight pieces. It is the difference between multiplicative and additive growth.

Where inheritance wins

Inheritance is not always wrong. It is the right choice when:

  • The relationship is genuinely subtyping, verified by the test above.
  • The hierarchy is shallow — one level — and closed.
  • The superclass is abstract and exists to define a contract, not to share implementation.

The last case is the most defensible: inheriting from an interface or a pure abstract class is a contract declaration, and brings no coupling to implementation.

Implementation inheritance versus interface inheritance

The distinction that dissolves much of the debate.

Interface inheritance — implementing a contract — is cheap and safe. Implementation inheritance — inheriting code — is where the three costs appear.

The advice "prefer composition" is, in practice, "prefer composition over implementation inheritance".

Mental Model

"Is a" versus "uses a" is insufficient. The better question is: can the caller treat me as the base type with no caveats whatsoever? If there is a caveat, compose.

When to Use

Inheritance when:

  • Substitutability is true without exception.
  • The hierarchy is shallow and the set of subtypes is known and closed.
  • The superclass is abstract and defines a contract.
  • The framework requires it — many do, and resisting costs more than accepting.

Composition for the rest, which is most cases.

When Not to Use

Inheritance to reuse code with no type relationship. The dominant mistake. If the only reason is to get the methods, compose.

Inheritance in a deep hierarchy. Each level multiplies the coupling to implementation and the difficulty of tracing where a behaviour comes from.

Inheritance with more than one axis of variation. It explodes combinatorially.

Composition when it produces blind delegation. If the composing class merely forwards twenty methods to the inner object, it is not composing — it is imitating inheritance with more code. There, either inheritance was adequate, or the boundary is wrong.

Composition taken to the extreme. A system where every behaviour is an injected object can become as hard to follow as a deep hierarchy.

Alternatives

  • First-class functions — when what varies is a simple behaviour, passing a function is lighter than either.
  • Traits or mixins — in languages that offer them, they sit between the two.
  • Duplication — for two cases with superficial similarity, see DRY.

Trade-offs

InheritanceComposition
Less code to writeMore explicit code
Automatic reuseManual delegation
Coupled to the base's implementationCoupled only to the interface
One axis of variationCombinable axes
Relationship fixed at compile timeConfigurable at runtime
Risk of breaking substitutabilityNo such risk

Failure Modes

Explosive hierarchy. A second axis of variation appears and the number of classes multiplies.

Refusing subclass. Overrides a method to throw — a declared Liskov violation.

Fragile base class problem. An internal change in the superclass breaks subclasses that were never touched.

Inheritance for convenience. UserService extends BaseService just to get a logging method.

Blind delegation. Composition that forwards everything, adding nothing.

Real-World Example

A reporting system had ReportBase with fetchData(), format() and send(), and eleven subclasses.

When the need arose to send the same report by email and by API, the hierarchy did not accommodate it: sending was tied to the report type. The solution adopted was a deliveryType parameter in send(), with a switch.

Six months later, formats: PDF, CSV, XLSX. A second switch.

In the end, ReportBase had 300 lines, two switch statements, and the eleven subclasses overrode between one and five methods each, in ways nobody could predict without reading all of them.

The reformulation by composition:

Report(dataSource, formatter, deliveryChannel)

sources: 11 implementations — the real variation
formatters: 3
channels: 2

Sixteen pieces instead of eleven classes with two embedded switch statements. And new combinations became configuration, not code.

The detail that matters: the eleven data sources remained eleven implementations of an interface. Contract inheritance stayed. What left was implementation inheritance.

A decision table

Facing a concrete choice, four questions in order:

QuestionIf yesIf no
Does substitutability hold without exception?continuecompose
Is there more than one axis of variation?composecontinue
Do you want to inherit contract or implementation?contract: inheritimplementation: continue
Is the hierarchy shallow and closed?inheritcompose

The first question eliminates most cases. The third is the one that saves most: inheriting from an interface or a pure abstract class is safe; inheriting code is not.

One edge case worth naming: frameworks that require inheritance. Extending a framework base class to get its behaviour is implementation inheritance with all its costs, and frequently there is no alternative. The mitigation is to keep that class thin — let it be an adapter that delegates to your own code, rather than the place where the logic lives.

  • SOLID — the Liskov substitution principle.
  • Encapsulation — what implementation inheritance breaks.
  • Interfaces — contract inheritance.
  • Code Smells — how to recognize problematic hierarchies.

Practical Exercise

Find the deepest hierarchy in your system. For each subclass, apply the substitutability test: is there any place that accepts the base and would break with this subclass?

Then count the axes of variation the hierarchy tries to accommodate. More than one is a sign that composition would serve better.

Interview Questions

  • When is inheritance the correct choice?
  • What is the fragile base class problem?
  • How can a Liskov violation get past the compiler and the tests?

Further Exploration

  • Gamma, Erich et al. Design Patterns. Addison-Wesley, 1994 — the original formulation of "prefer composition over inheritance".
  • Liskov, Barbara; Wing, Jeannette. A Behavioral Notion of Subtyping. TOPLAS, 1994.
  • Bloch, Joshua. Effective Java. 3rd ed., 2018 — "favor composition over inheritance".
Finished reading this document?Your progress is saved in this browser only.