8 min read
Object-Oriented Design Index
Tier 1 -- Foundations
Core Concepts
SOLID Principles
Creational Patterns
Structural Patterns
Tier 2
Tier 3
Tier 4
Tier 5
Object-Oriented Design Index
Tier 1 -- Foundations
Core Concepts
SOLID Principles
Creational Patterns
Structural Patterns
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."
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?
2. How It Works
mechanism:
- Define a base class with shared and behavior.
- A subclass extends it, inheriting all public/protected members.
- The subclass overrides methods to specialize behavior.
- The subclass is statically bound to the parent at compile time.
mechanism:
- Define a small, focused or class for a behavior.
- The owner class holds a reference (field) to that behavior object.
- The owner delegates calls to the composed object.
- 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
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?
3. Variants & Comparisons
| Approach | How It Works | Pros | Cons | Best For |
|---|---|---|---|---|
| Inheritance | Subclass extends base class; acquires all members | Simple, natural for true "is-a" relationships; polymorphism without extra wiring | Tight coupling; fragile base class problem; cannot change at runtime; deep hierarchies explode | Stable, true "is-a" hierarchies (e.g., IOException extends Exception) |
| Composition | Owner holds a reference to a behavior/helper object | Loose coupling; swappable at runtime; easier to test with mocks | More classes; more boilerplate (delegation methods) | Variable behaviors, cross-cutting concerns, reusing behavior across unrelated classes |
| Mixin / Multiple Inheritance | Class inherits from multiple bases (Python) | Reuse across unrelated hierarchies | Diamond problem; MRO complexity | Python: adding capabilities via mixins (e.g., LoggingMixin) |
| Interface + Delegation | Implement an interface, delegate to composed object | Full interface compliance + swappable impl | Forwarding boilerplate | Implementing 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.,
IllegalArgumentExceptionis anException). - 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
Loggeruses aFormatter). - 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'sjava.util.Stack) is a canonical mistake —Stackis not aVector, 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.
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?
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.
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?
6. Interview Cheat Sheet
Key sentences to say:
- "I follow 'favor over ' — I use only for true, stable is-a relationships; everything else I model as ."
- "Composition is more flexible because I can inject different behaviors at runtime; inheritance is statically bound at compile time."
- "The fragile base class problem is the main risk with inheritance — a change in the parent can silently break all subclasses."
- "Before subclassing, I ask: does LSP hold? Can a caller use a subclass instance everywhere a parent is expected without being surprised?"
- "Java's
Stack extends Vectoris 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.