Composition Over Inheritance

A design principle that favors assembling behavior from smaller, independent components rather than building deep class hierarchies. Originally articulated in the Gang of Four’s Design Patterns (1994): “Favor object composition over class inheritance.”

Inheritance creates tight coupling between parent and child classes. A change to a base class ripples through every subclass, and the relationship is frozen at compile time. Composition allows behavior to be assembled, swapped, and tested independently.

The Problem with Inheritance

Fragile base class: Modifying a parent class can break subclasses in unexpected ways, especially when subclasses depend on internal implementation details rather than public contracts.

Rigid hierarchies: Inheritance forces an is-a relationship that may not hold as requirements evolve. A Button that inherits from Rectangle cannot easily become circular without restructuring the hierarchy.

Diamond problem: Multiple inheritance creates ambiguity about which parent’s method to use. Languages that restrict to single inheritance force artificial hierarchies to work around the limitation.

Explosion of subclasses: When behavior varies along multiple axes, inheritance requires a subclass for every combination. Three rendering modes and four data sources produces twelve subclasses — composition handles it with seven components.

When to Use Composition

  • Behavior varies independently along multiple dimensions
  • Components need to be reused across unrelated classes
  • You need to swap behavior at runtime
  • The relationship is has-a rather than is-a
  • Testing requires isolating individual behaviors

When Inheritance Is Appropriate

  • Modeling a genuine type hierarchy (e.g., IOException extends Exception)
  • Framework extension points designed for inheritance (e.g., React class components before hooks)
  • Performance-critical paths where virtual dispatch overhead matters and the hierarchy is stable

Example

# Inheritance approach — rigid
class EmailNotifier(Notifier):
    def send(self, message): ...
 
class SlackNotifier(Notifier):
    def send(self, message): ...
 
class EmailAndSlackNotifier(EmailNotifier, SlackNotifier):  # Diamond
    def send(self, message): ...
 
# Composition approach — flexible
class NotificationService:
    def __init__(self, channels: list[NotificationChannel]):
        self.channels = channels
 
    def send(self, message):
        for channel in self.channels:
            channel.send(message)
 
# Assemble at runtime
service = NotificationService([EmailChannel(), SlackChannel()])

Relationship to Other Principles

Composition supports Single Responsibility — each component handles one concern. It also aligns with DRY by enabling reuse through assembly rather than hierarchy duplication.

Common Mistakes

  • Replacing all inheritance with composition dogmatically — some hierarchies are correct and stable
  • Creating overly granular components that fragment logic across dozens of tiny objects
  • Using composition but tightly coupling components through shared mutable state

References

  • Gamma, E., Helm, R., Johnson, R., Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software.
  • Bloch, J. (2018). Effective Java. 3rd Edition. Item 18: Favor composition over inheritance.