What does the design guideline "favor composition over inheritance" actually mean, and what is the difference between the two techniques?
answer
- is-a vs has-a
- inherits internals vs uses interface
- compile-time fixed vs runtime swappable
- accidental API leak (Stack extends ArrayList)
- forwarding boilerplate is the cost
basics
~20 sInheritance reuses code by making a new type a subtype of an existing one ("is-a"). Composition reuses code by holding another object as a field and calling it ("has-a"). Prefer composition: it couples types less and can change at runtime.
solid answer
~50 sBoth are code-reuse mechanisms. With inheritance, a subclass declares that it *is* a kind of its parent and automatically gets the parent's fields and methods; the relationship is fixed at compile time and the subclass sees (and depends on) the parent's internals and self-calls. With composition, a class holds a reference to a collaborator and forwards work to it; it depends only on the collaborator's public interface, the collaborator can be swapped at construction or runtime, and one class can combine several collaborators. "Favor composition" says: make reuse the default via has-a, and reach for inheritance only when you genuinely need subtype substitutability plus a stable, intentionally-designed base. Composition avoids the fragile base class problem, avoids the combinatorial explosion of subclasses when behavior varies along several axes, and keeps the subclass's public API from accidentally inheriting methods it shouldn't expose.
code
pseudocode · 11 lines// Inheritance: leaks the parent's whole API and depends on its internals
class Stack extends ArrayList {
fun push(x) = add(x) // callers can still do stack.remove(3)
}
// Composition: only the intended API is exposed
class Stack {
private val items = ArrayList() // has-a
fun push(x) { items.add(x) }
fun pop() = items.removeLast()
}go deeper
Define is-a vs has-a, give one example of each, and state that composition is the safer default.
Add the concrete failure modes — fragile base class, API leakage, single-inheritance limit, subclass explosion — and name the cost (forwarding boilerplate).
Separate subtyping from implementation reuse, tie the legitimate uses to LSP and "designed for extension", and mention the self-problem as composition's real limitation.
Frame it as a coupling/evolution decision: inheritance publishes your internals as a contract you must keep across versions, so it's mainly justified inside one ownership boundary or for closed hierarchies; discuss how you'd steer a codebase toward composition without a rewrite.
## The two mechanisms **Inheritance ("is-a", implementation inheritance).** A class *B* extends class *A*. *B* gets *A*'s state and behavior automatically, can override some of it, and — crucially — instances of *B* can be used anywhere an *A* is expected. Two things travel together here, and conflating them is the root of most confusion: 1. **Subtyping** — the promise "a *B* can stand in for an *A*" (the substitutability contract). 2. **Implementation reuse** — "*B* doesn't have to rewrite *A*'s code." **Composition ("has-a", delegation).** A class *B* holds a reference to an object of type *A* (usually typed by an interface) and calls it to do work. *B* exposes whatever API it wants; nothing leaks automatically. ``` // inheritance // composition class Stack extends ArrayList {} class Stack { private List items; push(x){items.add(x);} } ``` The inheritance version accidentally exposes `get(int)`, `remove(int)`, `insert` — a `Stack` that lets you poke at the middle is no longer a stack. The composition version exposes only `push`/`pop`. ## Why composition is the default - **Weaker coupling.** Composition depends on the collaborator's *published interface*. Inheritance depends on the base class's *implementation details*: which methods call which other methods, what invariants the constructor establishes, what protected state means. Those details are not usually part of a documented contract, so they change — and the subclass silently breaks. That's the **fragile base class problem**. - **Runtime flexibility.** The collaborator can be chosen by configuration, injected for tests, or swapped mid-flight (`setCompressor(gzip)`); a superclass is baked in at compile time. - **Multiple axes of variation.** If behavior varies along axes *storage × format × auth*, inheritance needs one subclass per combination (2×3×2 = 12 classes); composition needs 2+3+2 = 7 small parts assembled at runtime. - **No single-inheritance limit.** Most languages allow only one superclass but unlimited fields. Composition scales to many collaborators. - **Better encapsulation of the API surface.** A subclass inherits its parent's whole public API whether or not that makes sense (see the Stack example above). - **Testability.** A collaborator is trivially replaced with a stub/fake; overridden superclass behavior is not. ## What composition costs - **Boilerplate / forwarding code.** Wrapping a 40-method interface means writing 40 one-line forwarders (some languages help: Kotlin's `by`, Go's struct embedding, C#'s extension-plus-interface idioms, Groovy `@Delegate`). - **An extra indirection** — usually irrelevant, occasionally hot-path relevant. - **The self-problem.** In inheritance, a base method calling `this.foo()` dispatches to the override. A wrapped object calling its own `foo()` cannot see the wrapper — so behavior you expected to intercept isn't intercepted. Decorator-style designs have to be aware of this. - **More moving parts to wire up**, which is why dependency injection tends to accompany composition-heavy designs. ## When inheritance still wins Use it when there is a **true is-a relationship** that satisfies the **Liskov Substitution Principle** (every place that works with the base must keep working with the subtype), and when the base was **designed for extension** — documented self-calls, well-chosen extension points, ideally in the same codebase/team so the base and its subclasses evolve together. Interface inheritance (implementing a pure interface / abstract type with no inherited implementation) carries none of the fragility and is not what the guideline warns about; the guideline targets **implementation inheritance**. Sealed/closed hierarchies used for exhaustive matching (an expression AST: `Add | Mul | Literal`) are another legitimate, safe use — the whole hierarchy is known and owned in one place. ## The one-line rule of thumb Ask "is *B* a kind of *A* everywhere, forever?" If you're only after the code, use composition. If you also want the substitutability and you own the base, inheritance is fine.
- Does "favor composition over inheritance" also apply to implementing interfaces?No. The guideline targets *implementation* inheritance, where you inherit code and internal assumptions. Implementing a pure interface/abstract type adds a subtyping promise but no inherited implementation, so none of the fragility applies — that's normally encouraged.
- Give a concrete symptom that a hierarchy should have been composition.Overrides that throw `UnsupportedOperationException` or return "not applicable", subclasses that must be careful about the *order* they call `super`, or a subclass count that multiplies whenever a new independent option appears.
Inheritance is being born into a family — you get the whole genome, quirks included, and you can't change parents. Composition is hiring a contractor: you specify the job, you can fire and rehire, and you never inherit their personal habits.