Composition vs. Inheritance

8 min read

Reading Progress0%
Object-Oriented Design Index
Tier 1 -- Foundations
Tier 2
Tier 3
Tier 4
Tier 5
Object-Oriented Design Index
Tier 1 -- Foundations
Tier 2
Tier 3
Tier 4
Tier 5

Composition vs. Inheritance

1. What Is It?

models an "is-a" relationship: Dog is an Animal. A subclass acquires the parent's and implementation, extending or overriding behavior. models a "has-a" relationship: a Car has an Engine. An object holds a reference to another object and delegates work to it.

Both achieve code reuse and behavioral variation, but they differ fundamentally in and flexibility. Without understanding the trade-off, engineers default to for every reuse problem — leading to deep hierarchies that are brittle, hard to test, and resistant to change. The GoF book explicitly advises: "Favor object over class inheritance."


QUICK CHECK

A developer is building a notification system. They have an EmailSender class and now need a NotificationService that can send emails. They consider two approaches: (A) make NotificationService extend EmailSender, or (B) have NotificationService hold a reference to an EmailSender instance and call its methods. Which approach reflects composition, and what relationship does it model?

Choose one answer

2. How It Works

mechanism:

  1. Define a base class with shared and behavior.
  2. A subclass extends it, inheriting all public/protected members.
  3. The subclass overrides methods to specialize behavior.
  4. The subclass is statically bound to the parent at compile time.

mechanism:

  1. Define a small, focused or class for a behavior.
  2. The owner class holds a reference (field) to that behavior object.
  3. The owner delegates calls to the composed object.
  4. The composed object can be swapped at construction time or runtime.

Python — example:

from abc import ABC, abstractmethod


class Shape(ABC):
    @abstractmethod
    def area(self) -> float:
        ...

    @abstractmethod
    def perimeter(self) -> float:
        ...

    def describe(self) -> str:
        return f"{self.__class__.__name__}: area={self.area():.2f}, perimeter={self.perimeter():.2f}"


class Circle(Shape):
    def __init__(self, radius: float) -> None:
        self.radius = radius

    def area(self) -> float:
        import math
        return math.pi * self.radius ** 2

    def perimeter(self) -> float:
        import math
        return 2 * math.pi * self.radius


class Rectangle(Shape):
    def __init__(self, width: float, height: float) -> None:
        self.width = width
        self.height = height

    def area(self) -> float:
        return self.width * self.height

    def perimeter(self) -> float:
        return 2 * (self.width + self.height)

Python — example:

from abc import ABC, abstractmethod
from dataclasses import dataclass


class FlyBehavior(ABC):
    @abstractmethod
    def fly(self) -> str:
        ...


class WingFly(FlyBehavior):
    def fly(self) -> str:
        return "Flying with wings"


class NoFly(FlyBehavior):
    def fly(self) -> str:
        return "Cannot fly"


class QuackBehavior(ABC):
    @abstractmethod
    def quack(self) -> str:
        ...


class LoudQuack(QuackBehavior):
    def quack(self) -> str:
        return "QUACK!"


class Squeak(QuackBehavior):
    def quack(self) -> str:
        return "Squeak!"


@dataclass
class Duck:
    name: str
    fly_behavior: FlyBehavior
    quack_behavior: QuackBehavior

    def perform_fly(self) -> str:
        return self.fly_behavior.fly()

    def perform_quack(self) -> str:
        return self.quack_behavior.quack()

    def set_fly_behavior(self, fly_behavior: FlyBehavior) -> None:
        """Swap behavior at runtime — impossible with inheritance."""
        self.fly_behavior = fly_behavior


# Mix and match behaviors without new subclasses
mallard = Duck("Mallard", WingFly(), LoudQuack())
rubber_duck = Duck("RubberDuck", NoFly(), Squeak())

print(mallard.perform_fly())    # Flying with wings
print(rubber_duck.perform_fly()) # Cannot fly

# Runtime behavior change
mallard.set_fly_behavior(NoFly())
print(mallard.perform_fly())    # Cannot fly (e.g., injured duck)

Java — composition example:

// Behavior interfaces
public interface FlyBehavior {
    String fly();
}

public interface QuackBehavior {
    String quack();
}

// Concrete behaviors
public class WingFly implements FlyBehavior {
    @Override
    public String fly() { return "Flying with wings"; }
}

public class NoFly implements FlyBehavior {
    @Override
    public String fly() { return "Cannot fly"; }
}

public class LoudQuack implements QuackBehavior {
    @Override
    public String quack() { return "QUACK!"; }
}

// Owner class uses composition
public class Duck {
    private final String name;
    private FlyBehavior flyBehavior;
    private final QuackBehavior quackBehavior;

    public Duck(String name, FlyBehavior flyBehavior, QuackBehavior quackBehavior) {
        this.name = name;
        this.flyBehavior = flyBehavior;
        this.quackBehavior = quackBehavior;
    }

    public String performFly() { return flyBehavior.fly(); }
    public String performQuack() { return quackBehavior.quack(); }

    /** Swap behavior at runtime. */
    public void setFlyBehavior(FlyBehavior flyBehavior) {
        this.flyBehavior = flyBehavior;
    }
}

// Client
Duck mallard = new Duck("Mallard", new WingFly(), new LoudQuack());
System.out.println(mallard.performFly()); // Flying with wings
mallard.setFlyBehavior(new NoFly());
System.out.println(mallard.performFly()); // Cannot fly

QUICK CHECK

A Duck class is built using composition: it holds a FlyBehavior reference and delegates performFly() to it. Mid-session, the product requirements change so that an injured duck should no longer be able to fly. Which statement best describes how composition handles this compared to inheritance?

Choose one answer

3. Variants & Comparisons

ApproachHow It WorksProsConsBest For
InheritanceSubclass extends base class; acquires all membersSimple, natural for true "is-a" relationships; polymorphism without extra wiringTight coupling; fragile base class problem; cannot change at runtime; deep hierarchies explodeStable, true "is-a" hierarchies (e.g., IOException extends Exception)
CompositionOwner holds a reference to a behavior/helper objectLoose coupling; swappable at runtime; easier to test with mocksMore classes; more boilerplate (delegation methods)Variable behaviors, cross-cutting concerns, reusing behavior across unrelated classes
Mixin / Multiple InheritanceClass inherits from multiple bases (Python)Reuse across unrelated hierarchiesDiamond problem; MRO complexityPython: adding capabilities via mixins (e.g., LoggingMixin)
Interface + DelegationImplement an interface, delegate to composed objectFull interface compliance + swappable implForwarding boilerplateImplementing an interface using a composed object (Decorator, Proxy)

The fragile base class problem: When a base class method calls another method that a subclass overrides, changes to the base class can silently break all subclasses. avoids this because behavior is in a separate, independent object.


4. When to Use It (and When NOT To)

Favor when:

  • There is a true, stable "is-a" relationship that will not change (e.g., IllegalArgumentException is an Exception).
  • The Principle holds: the subclass can fully replace the parent without breaking callers.
  • The hierarchy is shallow (1–2 levels) and unlikely to grow.

Favor when:

  • The relationship is "has-a" or "uses-a" (e.g., a Logger uses a Formatter).
  • You need to swap or combine behaviors at runtime.
  • You want to reuse behavior across classes that have no "is-a" relationship.
  • The class hierarchy would otherwise explode combinatorially (e.g., FlyingSwimmingBird, FlyingNonSwimmingBird…).
  • You want code that is easy to unit test by injecting mock collaborators.

Anti-patterns:

  • Inheriting for code reuse alone: Stack extends Vector (Java's java.util.Stack) is a canonical mistake — Stack is not a Vector, and exposing vector operations on a stack breaks the .
  • Deep hierarchies (>3 levels): Each level adds . Beyond 3 levels, changes propagate unpredictably.
  • Using to add a feature to a class you don't control: Use the pattern () instead.

Decision trigger:

  • Ask: "Can I say '[SubClass] is a [ParentClass]' and have it be true in every context a caller uses it?" If yes, inheritance may fit. If the honest answer is "sort of", use composition.

QUICK CHECK

Your team needs to add logging capabilities to several unrelated service classes: OrderService, PaymentService, and InventoryService. A junior developer suggests creating a LoggableService base class and having all three extend it. What is the strongest argument against this design?

Choose one answer

5. Real-World Usage

1. Java Collections — AbstractList ( done right) ArrayList extends AbstractList directly, while LinkedList extends AbstractSequentialList (which itself extends AbstractList); both yield a true "is-a" relationship and are fully substitutable as lists. The hierarchy is shallow (one to two levels), and AbstractList provides default implementations of bulk methods without exposing irrelevant .

2. Java's java.util.Stack done wrong Stack extends Vector, which means a Stack exposes insertElementAt, setElementAt, and index-based access — operations that violate stack semantics. This is a classic example of inheriting for code reuse rather than for a true "is-a" relationship. Modern Java recommends Deque instead.

3. Python's logging module — A Logger holds a list of Handler objects (). Each Handler holds a Formatter (more composition). This design lets you combine any formatter with any handler without creating a subclass for every combination. Adding a new log destination requires a new Handler subclass — not a new Logger subclass.


QUICK CHECK

Java's Stack class extends Vector, which means a Stack instance exposes methods like insertElementAt() and index-based access. Why is this considered a design mistake?

Choose one answer

6. Interview Cheat Sheet

Key sentences to say:

  1. "I follow 'favor over ' — I use only for true, stable is-a relationships; everything else I model as ."
  2. "Composition is more flexible because I can inject different behaviors at runtime; inheritance is statically bound at compile time."
  3. "The fragile base class problem is the main risk with inheritance — a change in the parent can silently break all subclasses."
  4. "Before subclassing, I ask: does LSP hold? Can a caller use a subclass instance everywhere a parent is expected without being surprised?"
  5. "Java's Stack extends Vector is a textbook anti-pattern — it exposed vector operations on a stack, breaking the ."

Common follow-up questions:

Q: Isn't composition just more boilerplate? A: Yes, there's more delegation code. But that cost is usually worth it for testability (inject mocks), flexibility (swap at runtime), and stability (no fragile base class problem). For small, stable hierarchies, inheritance is fine.

Q: Can you use both? A: Yes, this is common. A class can inherit from a base (for ) and compose behaviors (for variability). The pattern is exactly this: a context class with a composed field, where the strategy is an implemented by interchangeable algorithm classes.

Q: What about Python mixins? A: Mixins are a limited form of multiple inheritance for adding orthogonal capabilities (logging, serialization). They work best when the mixin adds behavior without adding , and when the MRO is simple. For complex behavior sharing, composition is still cleaner.

Connections to other concepts:

  • Strategy pattern is composition in action — variable algorithms injected as objects.
  • pattern uses composition to wrap and extend objects without subclassing.
  • LSP is the litmus test for inheritance — if LSP doesn't hold, don't use inheritance.
  • DIP pushes toward composition by making classes depend on abstractions (interfaces), not concrete parent classes.
Glossary History

Click dotted jargon to save explanations here.

Glossary History

Click dotted jargon to save explanations here.