Command Palette

Search for a command to run...

Level 2 · 20 min

Composition vs Inheritance

"Favor object composition over class inheritance" is one of the most important principles from the GoF Gang of Four. Inheritance creates tight compile-time coupling; composition is more flexible and testable.

When Inheritance is Appropriate

Inheritance models an is-a relationship where LSP holds — substituting the subclass never breaks the base class contract. It's appropriate when: subclasses genuinely are specializations of the base, the hierarchy is shallow (2-3 levels max), and the base class is designed for extension (not just happened to have useful code). Extending to reuse code without an is-a relationship is abuse of inheritance.

Favor Composition

Composition (has-a) combines objects to achieve complex behavior. More flexible than inheritance: the composed object can be changed at runtime, composed classes can be tested independently, and no fragile base class problem. A Car has-a Engine rather than Car extends Engine. Composition is the mechanism behind Strategy, Decorator, and most GoF patterns. Bloch's Item 18 (Effective Java) demonstrates the hazard with the concrete InstrumentedHashSet example: a subclass of HashSet that counts insertions unexpectedly returns 6 after addAll(List.of("Snap","Crackle","Pop")) instead of 3, because HashSet.addAll internally calls add() — an implementation detail the subclass must not depend on. Bloch's conclusion: "Inheritance is appropriate only in circumstances where the subclass really is a subtype of the superclass... if you are tempted to have a class B extend class A, ask yourself the question: is every B really an A? If you cannot truthfully answer yes to this question, B should not extend A. B should contain a private instance of A and expose a different API."

Decorator via Composition

The Decorator pattern is composition in action — it wraps an object with the same interface and adds behavior. LoggingOrderService wraps OrderService and adds logging around all methods. This is far more flexible than LoggingOrderService extends OrderService — you can compose multiple decorators and change them at runtime without subclassing.

Key Takeaways

  • Prefer composition when you need to reuse behavior — composition is more flexible, testable, and maintainable.
  • Inheritance for is-a relationships with LSP compliance. Composition for has-a and behavioral sharing.
  • Deep inheritance hierarchies (5+ levels) are almost always a design problem — flatten with composition.

Code example

// Inheritance abuse: reusing code without is-a
class Stack<T> extends Vector<T> {  // java.util.Stack mistake
  // Stack IS-NOT-A Vector — it only uses 3 of Vector's 50 methods
  // But now Stack exposes get(index), add(at, element) — broken abstraction
}

// Composition: Stack HAS-A storage
class Stack<T> {
  private final Deque<T> storage = new ArrayDeque<>();

  void push(T item) { storage.push(item); }
  T pop() { return storage.pop(); }
  T peek() { return storage.peek(); }
  boolean isEmpty() { return storage.isEmpty(); }
  // Only exposes Stack operations, not Deque's full API
}