Composition Over Inheritance

Beginner · 10 min read · ▶ live playground · ✦ checkpoint

A warehouse alarm starts with a bell. Then one site wants a voice, another wants a flashing light, and a third wants both. A subclass appears for every combination until choosing two small behaviors requires navigating a family tree.

Composition means building an object's behavior by holding other objects that do parts of the work. It models a has-a relationship: a warehouse alarm has a sounder and has a signal light. Inheritance models is-a; composition assembles has-a.

The phrase composition over inheritance is a preference to check whether swappable parts express the design better before adding another child class. It is not a ban on inheritance.

Compare a growing tree with assembled parts

An inheritance tree fixes choices into class names. LoudFlashingAlarm receives everything above it, whether those inherited choices still fit or not. A composed alarm keeps sound and light as separate decisions.

class diagram

An alarm tree beside a composed alarm

An alarm tree beside a composed alarm
  1. 1The inheritance version starts with one Alarm behavior.
  2. 2LoudAlarm extends Alarm to add one sound choice to the family tree.
  3. 3LoudFlashingAlarm adds a second choice by growing the hierarchy another level.
  4. 4The composed WarehouseAlarm keeps sound and light as separate fields.
  5. 5Composition: the filled diamond says WarehouseAlarm has a Sounder part.
  6. 6A second composed part lets the alarm vary its light without changing its sound.

The solid hollow triangles on the left point to parent classes. The filled diamonds on the right sit at the whole, WarehouseAlarm; each line connects the whole to one owned part.

The interfaces make each part replaceable without making the alarm inherit from it. A warehouse alarm is not a sounder and is not a signal light. It has those parts and asks them to work.

Supply behavior through the constructor

Dependency injection means an object receives the other objects it needs from outside instead of constructing one fixed choice internally. Here, the constructor receives a Sounder and a SignalLight. No framework or special container is required.

interface Sounder {
  sound(): string;
}
 
class BellSounder implements Sounder {
  public sound(): string {
    return "BELL: ring ring";
  }
}
 
class VoiceSounder implements Sounder {
  public sound(): string {
    return "VOICE: evacuate now";
  }
}
 
interface SignalLight {
  flash(): string;
}
 
class BlueSignalLight implements SignalLight {
  public flash(): string {
    return "BLUE LIGHT: flash";
  }
}
 
class NoSignalLight implements SignalLight {
  public flash(): string {
    return "LIGHT: off";
  }
}
 
class WarehouseAlarm {
  private sounder: Sounder;
  private light: SignalLight;
 
  public constructor(sounder: Sounder, light: SignalLight) {
    this.sounder = sounder;
    this.light = light;
  }
 
  public trigger(): string {
    return `${this.sounder.sound()} | ${this.light.flash()}`;
  }
}
 
const loadingBayAlarm = new WarehouseAlarm(
  new BellSounder(),
  new BlueSignalLight(),
);
 
const quietOfficeAlarm = new WarehouseAlarm(
  new VoiceSounder(),
  new NoSignalLight(),
);
 
console.log(loadingBayAlarm.trigger());
console.log(quietOfficeAlarm.trigger());

BELL: ring ring | BLUE LIGHT: flash VOICE: evacuate now | LIGHT: off

WarehouseAlarm contains no branch for bell versus voice or blue light versus no light. It calls the same small contracts each time. The concrete objects supplied to the constructor decide what happens.

When trigger() calls this.sounder.sound(), it delegates the sound work: the alarm remains responsible for the whole trigger flow while a held object performs one part. This keeps the public action cohesive without forcing one class to know every sound algorithm.

The two choices can now vary independently. A voice with a blue light needs no VoiceBlueLightAlarm subclass. Supply new VoiceSounder() beside new BlueSignalLight(). Adding a new amber light also leaves every sounder unchanged.

Avoid hierarchy lock-in

Hierarchy lock-in happens when independent choices are encoded in ancestry, so changing or combining them requires another subclass. With sound volume, spoken language, light color, and flashing rhythm all in the tree, class names start representing combinations rather than stable kinds of thing.

Composition turns those choices into parts. Each part has a focused contract, and the constructor makes the required parts visible. A test can also supply a small stand-in sounder that returns known text without constructing a real alarm device.

The filled diamond carries a stronger claim than “calls this object once.” It says the parts belong to the whole in this model. The alarm holds its sounder and light for its working lifetime. The next lesson will separate this owned relationship from looser references and temporary method use.

When not to replace inheritance

Inheritance is still the clearer model for a genuine is-a relationship with a shared contract. MemberTicket is an AdmissionTicket, and callers benefit from treating every ticket through that parent type. Replacing the ticket hierarchy with a bag of tiny objects would hide a useful category rather than improve it.

Composition also has a cost. Splitting every three-line calculation into its own interface creates more names, constructors, and object wiring than the behavior earns. If a part never varies, has no rule of its own, and is meaningful only as a few lines inside the whole, keeping it local may be clearer.

Prefer a shallow inheritance relationship when the model is truly hierarchical. Prefer composition when you are combining independent capabilities, want to swap them, or would otherwise create subclasses for every option. Judge the relationship, not the slogan.

Checkpoint

Answer all three to mark this lesson complete

You can now choose between a lineage and an assembly instead of reaching for extends by habit. Next, you will name the four object relationships precisely and read their different UML lines without overstating what the code proves.

+50 XP on completion