Low-level designCore6 min
Composition over inheritance
Build an object from parts it holds and calls, instead of from a parent it extends. Each part varies on its own, parts can be swapped while the program runs, and two independent choices no longer multiply into a class for every combination.
Robots with drives and grippers
Wheels or tracks, a claw or suction: one subclass for each pairing.
1 · The situation
Warehouse robots, one class per kind
A warehouse runs fetching robots. Each one has a drive, wheels or tracks, and a gripper, a claw or a suction cup. The first version models each kind of robot as a subclass of Robot, which supplies fetch() and leaves moveTo() and pick() to each kind:
abstract class Robot {
private final String name;
protected Robot(String name) {
this.name = name;
}
abstract String moveTo(String bay);
abstract String pick(String item);
String fetch(String item, String bay) {
return name + ": " + moveTo(bay) + ", " + pick(item);
}
}
class WheeledClawRobot extends Robot {
WheeledClawRobot(String name) {
super(name);
}
String moveTo(String bay) {
return "rolls to " + bay;
}
String pick(String item) {
return "grips the " + item;
}
}
class TrackedSuctionRobot extends Robot {
TrackedSuctionRobot(String name) {
super(name);
}
String moveTo(String bay) {
return "crawls to " + bay;
}
String pick(String item) {
return "lifts the " + item + " by suction";
}
}Drawn from the code: four subclasses, one for each pairing of drive and gripper.
R1: rolls to A3, grips the crate R2: crawls to B7, lifts the glass sheet by suction
2 · The change
Combinations multiply, and tools can't change
Two requests arrive. The warehouse buys legged robots for the stairs, and a magnetic gripper for steel parts. And robots should change grippers during a shift: R2 has a suction cup now, but its next job is a crate.
Both are hard with subclasses:
- Every new option multiplies the classes. Each drive must be paired with each gripper, so three drives and three grippers need nine subclasses, and every "rolls to" and "grips the" is written once per pairing.
- An object can't change its class. R2 is a
TrackedSuctionRobotfor life; giving it a claw means replacing the robot with a different object.
Classes needed for the drives and grippers. Subclasses grow with the product of the options; parts grow with their sum.
3 · The fix
A robot made of a drive and a gripper
Each choice that varies becomes its own small family of classes, and a robot has one of each instead of being one combination:
A Drive interface with Wheels and Tracks, and a Gripper interface with Claw and Suction. Each piece of behavior is written once.
interface Drive {
String moveTo(String bay);
}
class Wheels implements Drive {
public String moveTo(String bay) {
return "rolls to " + bay;
}
}
interface Gripper {
String pick(String item);
}
class Claw implements Gripper {
public String pick(String item) {
return "grips the " + item;
}
}R1: rolls to A3, grips the crate R2: crawls to B7, lifts the glass sheet by suction R2: crawls to C1, grips the crate
The same two fetches, then R2 with a claw. Legs or a magnet would be one new class each.
4 · Has-a or is-a
What each approach gives you
One Robot holding a Drive and a Gripper. The practice checks read each part as a Strategy: an interface with several classes, given to the robot from outside.
Aspect | Inheritance | Composition |
|---|---|---|
Relationship | Is a kind of | Has a part |
Fixed when? | When the code is written | When the object is built, and changeable later |
Two varying choices | One class per combination | One class per option |
Coupling | To the parent's code, including its insides | Only to the part's interface |
Cost | Little code to write at first | A forwarding call for each use of a part |
5 · When to inherit
Composition first, inheritance on purpose
Composition is the safer default, not a ban on inheritance. Inherit when the subclass really is a kind of the parent everywhere it's used, the parent was designed to be extended, and there's one dimension of variation, not two. The page Inheritance shows what goes wrong when a subclass leans on how its parent works inside; holding a part instead depends only on what the part promises.
Composition's cost is the forwarding: if Robot needed twenty of the drive's methods, it would need twenty one-line methods passing calls along. That's a hint the robot should expose the part, or that the part's interface is too big.
6 · In the patterns
Most patterns are composition
Many design patterns on later pages are composition with a particular purpose:
- Strategy: a part that does one job in interchangeable ways, like the drive and the gripper here.
- Decorator: a part wrapped by another object of the same interface, which adds behavior.
- Bridge: two hierarchies kept separate so each can grow, when the robots form a hierarchy of their own as well as their parts.
7 · In practice
A garage with a pricing part
In the parking-lot problem, a lot that subclasses itself for each pricing scheme (a match-day lot, a weekday lot) multiplies classes and can't switch schemes on match day. A lot that holds a pricing part, given to it from outside, can. The checks look for exactly that.
Design the parking lot from parts.
Check yourself
4 questions
Takeaways
Remember this
- Composition builds an object from parts it holds and calls; inheritance builds it from a parent it extends.
- Two choices that vary independently multiply subclasses but only add parts.
- A part held in a field can be swapped while the program runs; a class can't.
- Composition couples you to a part's interface, inheritance to the parent's insides.
- Composition costs forwarding calls; inherit only for a true "is a kind of".