Design Patterns, Explained Simply · Foundations: Thinking in Patterns
Program to Interfaces, Favour Composition
The two ideas behind almost every pattern: depend on what an object can do, not what class it is, and build behaviour by combining objects instead of deep inheritance trees.
The problem
A game has robots. You start with Robot, then add WalkingRobot and FlyingRobot as subclasses. Then the designers want a robot that walks and flies, one that swims, one that swims and flies, one that changes from walking to flying mid-level...
With inheritance, every combination becomes a new subclass: WalkingSwimmingRobot, FlyingSwimmingRobot, and so on. The number of classes explodes, the same movement code is copied between them, and a robot can't change how it moves at runtime because its class is fixed.
Real-world analogy
A power drill doesn't come in a "drill that screws" model and a "drill that sands" model. It has a standard chuck (the interface) and you snap in whichever bit you need (composition). Any bit that fits the chuck works, and you can swap bits mid-job.
The idea
The Gang of Four book opens with two principles. Nearly every pattern in this course applies one or both:
- Program to an interface, not an implementation. Code that uses a robot should depend on "something that can
move()", not on the concrete classFlyingRobot. Then any object withmove()works. - Favour object composition over class inheritance. Instead of a robot being a flying robot (inheritance), a robot has a movement object (composition). Combinations come from plugging objects together, not from new subclasses.
Inheritance is still right for real "is-a" relationships with shared behaviour that never varies independently. The trouble starts when you use it to mix and match features.
In dynamic languages, "interface" can just mean "has the right method" (duck typing in Python and PHP, structural typing in TypeScript). In Java it is usually an explicit interface.
The code
The robot gets its movement from a separate object, so new combinations need no new robot classes, and movement can change at runtime.
class Walk:
def move(self, name):
return f"{name} walks on two legs"
class Fly:
def move(self, name):
return f"{name} flies with rotors"
class Swim:
def move(self, name):
return f"{name} swims with a propeller"
class Robot:
def __init__(self, name, movement):
self.name = name
self.movement = movement # composition: the robot HAS a movement
def move(self):
return self.movement.move(self.name)
scout = Robot("Scout", Walk())
print(scout.move())
scout.movement = Fly() # change behaviour at runtime, no new class
print(scout.move())
print(Robot("Diver", Swim()).move())
Output
Scout walks on two legs Scout flies with rotors Diver swims with a propeller
interface Movement {
move(name: string): string;
}
class Walk implements Movement {
move(name: string) { return `${name} walks on two legs`; }
}
class Fly implements Movement {
move(name: string) { return `${name} flies with rotors`; }
}
class Swim implements Movement {
move(name: string) { return `${name} swims with a propeller`; }
}
class Robot {
// composition: the robot HAS a movement
constructor(public name: string, public movement: Movement) {}
move(): string {
return this.movement.move(this.name);
}
}
const scout = new Robot("Scout", new Walk());
console.log(scout.move());
scout.movement = new Fly(); // change behaviour at runtime, no new class
console.log(scout.move());
console.log(new Robot("Diver", new Swim()).move());
Output
Scout walks on two legs Scout flies with rotors Diver swims with a propeller
public class Main {
public static void main(String[] args) {
Robot scout = new Robot("Scout", new Walk());
System.out.println(scout.move());
scout.movement = new Fly(); // change behaviour at runtime, no new class
System.out.println(scout.move());
System.out.println(new Robot("Diver", new Swim()).move());
}
}
interface Movement {
String move(String name);
}
class Walk implements Movement {
public String move(String name) { return name + " walks on two legs"; }
}
class Fly implements Movement {
public String move(String name) { return name + " flies with rotors"; }
}
class Swim implements Movement {
public String move(String name) { return name + " swims with a propeller"; }
}
class Robot {
final String name;
Movement movement; // composition: the robot HAS a movement
Robot(String name, Movement movement) {
this.name = name;
this.movement = movement;
}
String move() { return movement.move(name); }
}
Output
Scout walks on two legs Scout flies with rotors Diver swims with a propeller
Run this on your computer — see “Get Started” for setup.
<?php
interface Movement {
public function move(string $name): string;
}
class Walk implements Movement {
public function move(string $name): string { return "$name walks on two legs"; }
}
class Fly implements Movement {
public function move(string $name): string { return "$name flies with rotors"; }
}
class Swim implements Movement {
public function move(string $name): string { return "$name swims with a propeller"; }
}
class Robot {
// composition: the robot HAS a movement
public function __construct(public string $name, public Movement $movement) {}
public function move(): string {
return $this->movement->move($this->name);
}
}
$scout = new Robot("Scout", new Walk());
echo $scout->move(), "\n";
$scout->movement = new Fly(); // change behaviour at runtime, no new class
echo $scout->move(), "\n";
echo (new Robot("Diver", new Swim()))->move(), "\n";
Output
Scout walks on two legs Scout flies with rotors Diver swims with a propeller
Three movement classes cover every robot. With inheritance, walk/fly/swim combinations would need up to seven subclasses, and none could switch at runtime.
Common mistakes
- "Composition means never inherit." Inheritance is fine for genuine "is-a" relationships with stable shared code, like a framework's base controller.
- Interfaces for everything. An interface with one implementation that will never have a second adds a file and no flexibility. Introduce it when a second implementation or a test double appears.
- Deep hierarchies "for reuse". Reusing code through a base class couples every subclass to the base. A change there ripples everywhere (the fragile base class problem).
Key points
- Program to an interface: depend on what an object can do, not on its concrete class.
- Favour composition: give objects other objects to delegate to, instead of subclassing for every combination.
- Composition allows runtime changes and avoids class explosion.
- Use inheritance for true "is-a" relationships, not to mix features.
- These two ideas power Strategy, Decorator, Bridge, State and most other patterns in this course.
Practice questions
What does "program to an interface, not an implementation" mean in practice?
Answer
Code that uses an object should depend on the operations it needs (an interface, or in dynamic languages just the method names), not on a concrete class. For example, a Robot asks for any Movement with move(), so Walk, Fly or a test double all work without changing Robot.
Why can inheritance lead to a "class explosion"?
Answer
When you use subclasses to combine independent features (walking, flying, swimming), every combination needs its own subclass, so the count grows multiplicatively. Composition gives each feature its own object, so you need one class per feature and combine them freely.
When is inheritance still the right choice?
Answer
For a real "is-a" relationship where the subclass is a specialised version of the parent and the shared behaviour does not vary independently, for example a framework's base Controller or an IOException subclass. Avoid it when you are using it only to reuse code or mix features.
How does duck typing relate to programming to an interface?
Answer
In Python, PHP (without type hints) and TypeScript (structural typing), any object with the right methods fits, even without declaring an interface. The principle is the same: depend on behaviour, not on a specific class. Java expresses it with an explicit interface.
Open in the app to mark lessons complete, take notes and track your progress.