Module 01 · Object thinking · Lesson 2 of 4
Composition over inheritance
Why designs built from small composed objects absorb change better than deep class hierarchies: the class explosion inheritance causes, delegation and strategies as the alternative, and when inheritance is still right.
Why it matters
Inheritance looks like the obvious way to share code and model variation, and it works for the first two variants. By the fifth, a hierarchy has subclasses for every combination of features, a change in the base class breaks children nobody remembered, and a new requirement that cuts across the hierarchy has no good home. "Favour composition over inheritance" is the oldest advice in object-oriented design for exactly this reason, and it is the single most useful habit in low-level design interviews.
The idea
Inheritance builds a class by extending another: the child is a kind of the parent and gets its behaviour. Composition builds a class by holding other objects and delegating to them: the class has a part that does some of the work. Composition varies behaviour by plugging in different parts, at construction or at runtime, instead of by choosing a different subclass.
The class explosion
Model notifications that vary along two independent dimensions, channel (email, SMS, push) and urgency handling (send now, batch hourly, quiet hours). With inheritance, every combination needs its own subclass, and a new channel multiplies the count again.
interface Channel { void deliver(User u, Message m); }
interface Schedule { void submit(Runnable send); }
final class Notifier {
private final Channel channel;
private final Schedule schedule;
Notifier(Channel channel, Schedule schedule) { this.channel = channel; this.schedule = schedule; }
void notify(User u, Message m) { schedule.submit(() -> channel.deliver(u, m)); }
}
var urgent = new Notifier(new SmsChannel(gateway), new SendNow());
var digests = new Notifier(new EmailChannel(smtp), new BatchedHourly(scheduler));
Adding WhatsApp is one new Channel; adding "only on weekdays" is one new Schedule. Nothing existing changes.
Why composition holds up
| Concern | Inheritance | Composition |
|---|---|---|
| Varying along several dimensions | One subclass per combination | One small class per option on each dimension |
| Coupling | Child depends on the parent's implementation details | Holder depends only on the part's interface |
| Changing behaviour at runtime | Impossible; the class is fixed | Swap the part (a new strategy, a different channel) |
| Testing | Must construct the whole hierarchy | Pass a fake part |
| Reuse | All of the parent, wanted or not | Exactly the part you need |
Delegation without boilerplate
The cost of composition is forwarding methods: a wrapper that adds behaviour to a list must pass through every list method it does not change. Languages help: Kotlin's by delegation, Go's struct embedding, and decorator base classes reduce the boilerplate. More often the boilerplate is a sign the wrapper is too broad; a narrow interface for what callers actually need is smaller to delegate.
When inheritance is right
Inheritance is still the right tool when the relationship is a genuine subtype that will be used polymorphically, and the parent was designed for extension: a framework base class with documented hooks, an abstract class implementing a fixed algorithm with specific steps left open (the template method pattern), or a sealed hierarchy of variants for pattern matching. The test is whether every child can stand in for the parent everywhere without surprises, which the Liskov substitution lesson makes precise.
Trade-offs
Composition costs a few more classes, some forwarding code and a wiring step that assembles the parts. In return, variation stays independent, each part is tested alone, and new behaviour arrives as new classes rather than edits. Inheritance is less code for a genuine single-dimension hierarchy and becomes the expensive choice the moment a second dimension appears.
In an interview
When you notice two things varying independently, say it: "Vehicle type and pricing rule vary independently, so Vehicle is a small hierarchy for size, and the lot holds a PricingStrategy it delegates to. Adding weekend pricing is a new strategy, not new vehicle subclasses." That one sentence demonstrates the principle better than naming it.
Check yourself
A report generator has subclasses PdfDailyReport, PdfWeeklyReport, CsvDailyReport and CsvWeeklyReport. Product asks for Excel output and a monthly period. What design avoids the growing hierarchy?