Hermes Wiki

Composition vs Inheritance

Concept

Inheritance and composition are the two mechanisms object-oriented languages give you for building new behavior out of existing behavior, and they encode two different relationships. Inheritance (class Dog extends Animal) models "is-a": a Dog is a kind of Animal, gets Animal's methods and fields automatically, and can be used anywhere an Animal is expected (the Liskov Substitution Principle's whole premise). Composition (class Car { private Engine engine; }) models "has-a": a Car isn't a kind of Engine, it contains one and delegates to it for the behavior it needs.

The "favor composition over inheritance" guidance — stated explicitly in Design Patterns (Gamma, Helm, Johnson, Vlissides, 1994) as one of its two foundational principles alongside "program to an interface, not an implementation" — comes from a specific structural weakness inheritance has that composition doesn't: inheritance breaks encapsulation across the subclass boundary. A subclass's behavior depends not just on the base class's public interface but on its implementation details — which internal methods call which other internal methods. When a base class changes how one of its methods is implemented internally, every subclass that happened to depend on the old internal call pattern can silently break, even though the base class's public contract didn't change. This is the well-documented "fragile base class problem," and it gets worse the deeper the hierarchy goes, since a change at the root can ripple through every level beneath it.

Composition doesn't have this problem because a class holding a reference to another object only ever depends on that object's public interface — there's no privileged access to internals, so changes to the held object's implementation can't reach in and break the container. The tradeoff is that composition requires deliberately writing delegation — the containing class has to explicitly forward calls to the object it holds — where inheritance gets that forwarding for free by construction.

Tradeoffs

Approach Benefit Cost
Inheritance Free method/field reuse, natural fit for genuine is-a hierarchies with substitutability, polymorphic dispatch built in Fragile base class problem — subclasses can depend on base-class internals, not just its interface; hierarchies get brittle and hard to refactor as they deepen; most OO languages only allow single inheritance, so it consumes the class's one "extends" slot
Composition No fragile-base-class coupling — only the public interface of the held object is ever depended on; a class can compose many objects at once (no single-inheritance limit); behavior can be swapped at runtime by swapping the held reference Requires explicit delegation code for every method that should forward — more boilerplate unless the language has delegation sugar; loses free polymorphic substitutability unless paired with an explicit interface
Mixins / traits (where the language supports them) Reuse across multiple hierarchies without a single-inheritance chain, less boilerplate than manual delegation Not available in every language (no true multiple inheritance in Java/C#); can reintroduce base-class-style coupling if a mixin depends on state/methods it assumes the including class provides
Interface + composition (compose a dependency, expose it through an interface the container implements) Combines composition's decoupling with polymorphic substitutability for the container itself Most code to write of the options here — both the interface and the delegation

The deeper cost asymmetry: inheritance's cost (fragility) is invisible at the moment you write extends and only shows up later, when the base class changes for a reason that has nothing to do with the subclass. Composition's cost (delegation boilerplate) is visible immediately, at write time. That asymmetry is exactly why the guidance defaults to composition — a cost you pay and see immediately is easier to manage than one that's deferred and can surface as an unrelated-looking bug far from its actual cause.

When to use / when not to

  • Default to composition: reach for it whenever a class needs to use another object's behavior, which covers most real reuse scenarios.
  • Use inheritance only for a genuine, stable is-a relationship where the subclass really can be substituted anywhere the base type is expected (Liskov Substitution Principle) and where the base class's contract is unlikely to need to change in ways that ripple into implementation details subclasses depend on.
  • A good litmus test from Joshi/GoF-derived guidance: ask "does B need to be an A, or does B just need to use some of A's behavior" — if the honest answer is the second, that's composition (B holds a reference to an A), not inheritance, even if inheritance would technically compile.
  • Don't reach for inheritance purely to avoid writing a few lines of delegation code — that's optimizing for the wrong axis; the boilerplate cost is paid once and stays visible, the fragile-base-class cost is paid unpredictably later.
  • Don't build a deep inheritance chain (more than two or three levels) as a first design — depth compounds the fragile base class problem, and a deep hierarchy is a strong signal the design should be flattened into composed, focused objects instead.

Common pitfall

Reaching for inheritance to reuse a chunk of behavior that has no real is-a relationship to the class needing it — the classic example is subclassing a Stack from a language's built-in growable-array/List type purely to get its storage operations for free (java.util.Stack extends Vector is the textbook cautionary example), which then exposes the full List interface (insert-in-the-middle, index-based access) on something that's supposed to be a strict LIFO structure. Callers can bypass the stack's actual invariant entirely, because inheritance handed them every public method of the base class whether or not it belonged on the subtype's contract. Composition would have exposed only push/pop/peek by design.

Engineering Lens

The design-review question that actually distinguishes a correct choice from a convenient one isn't "does this compile with extends" — it's "if the base class's internal implementation changes next quarter for a reason unrelated to this subclass, does this subclass still behave correctly." If the honest answer requires knowing something about the base class beyond its documented public contract, that's the fragile-base-class problem already present, whether or not it's caused a visible bug yet. Composition doesn't eliminate the need for careful interface design, but it does guarantee that dependency can only ever be on the public contract — which is exactly the property that makes a codebase's coupling graph legible enough to refactor safely years later.

  • Decorator Pattern — a canonical example of composition used specifically to add behavior at runtime, the scenario GoF cites directly when recommending composition over inheritance
  • Strategy Pattern — composes a swappable behavior object rather than hardcoding it via subclassing, letting behavior change at runtime instead of being fixed at compile time by class hierarchy

Sources

Hermes Wiki