The idea: is-a or has-a
There are two common ways to reuse code between classes. With inheritance a class
is a kind of its parent: Duck extends Bird and gets the parent's methods for free.
With composition a class has other objects and hands work to them:
a Bird holds a FlyBehavior and calls it. The page builds the same small bird
game both ways, makes the same changes to both, and counts what each change costs.
fly() by walking up the class tree; composition calls the part the object holds.How to read the page
- The left lane is the inheritance design, the right lane the composition design. Each shows its
class diagram, the flock built in
main(), and a scoreboard. - Add Goose, Add Penguin, Add Robots and Break Duck's Wing make that change in both designs at once; one Step moves each lane by one
edit. Fly All runs
b.fly()for every bird, two steps per bird. - Header colours: teal = a new class, orange = a class that already worked and had to be edited, yellow = the code running now, red = an error.
- The scoreboard counts classes, edits to working classes (each time a class that was already written is changed) and copied methods.
Inheritance: a subclass is a parent
At the start Bird defines fly() ("flap wings") and an abstract sound().
Duck and Sparrow only override sound(). A call like
duck.fly() is found by method lookup: start at the object's class,
Duck, and walk up until a class defines fly().
When the new class really is a kind of the parent, this is the shortest code there is. A goose flaps and
honks: one class, Goose extends Bird, with one method. Composition needs a new
Honk part and a line of wiring, and it started with twice as many classes.
(Demo: add a Goose)
When a subclass can't keep the promise
A penguin is a bird, so Penguin extends Bird looks right. But Bird promises
fly(), and a penguin can't fly. The usual quick fix overrides fly() to throw.
It compiles, and then any code written for Bird, like the loop in main(), crashes
when it meets the penguin. That breaks the Liskov substitution principle: code that works
with a parent must keep working with every subclass. See SOLID: Liskov
Substitution for the square and rectangle version.
FlyingBird layer, and every flying class is edited to extend it.The real fix is a new layer: FlyingBird takes fly(), Bird keeps only what
every bird does, and Duck, Sparrow (and Goose) are edited to extend
FlyingBird. Code that flies birds must now ask for a List<FlyingBird>. That is
3 or 4 classes that already worked, opened and retested. The composition lane adds NoFly and
Squawk and touches nothing. (Demo: add a Penguin)
Class explosion
Now every species gets a robot version that flies by rocket. Inheritance has one place to put
behaviour: the class tree. So each robot is a subclass of its species (RobotDuck extends Duck
keeps the quack) and each one needs the same rocket fly(). Java gives a class only one parent,
so the rocket code can't be shared through the tree; it is copied. With 3 species that is 3 new classes and
2 copies; composition writes one RocketFly class and wires it. (Demo: robot birds)
The general rule: when behaviour varies along two independent axes, here how a bird flies and how it sounds, subclasses grow like the product of the choices and parts grow like the sum. With a third axis (say, how a bird swims) the product gets out of hand fast.
Composition: has-a, and delegation
In the composition design there is one Bird class. It holds two fields typed by interfaces,
FlyBehavior fly and SoundBehavior sound, and its fly() just calls
fly.fly(). That forwarding call is delegation. A kind of bird is not a class
any more but a choice of parts: new Bird(new FlyWithWings(), new Quack()). Each behaviour is a
small class behind an interface, which is the Strategy pattern.
The price is visible on the scoreboard at the start: 6 classes against 3, and every call takes one more hop. Wrapping an object in another that has the same interface, as in the Decorator and Proxy patterns, is composition too.
Changing behaviour while the program runs
An object's class is fixed when it is created. If the duck breaks a wing during the game, the inheritance
design can only give Duck a hurt flag and its own fly() that checks it:
an edit to a working class, and every duck, robot ducks too, now carries that flag. The composition design
calls duck.setFly(new NoFly()): the same object, a new part, no class edited.
(Demo: a duck breaks its wing)
What the demos show
| Demo | Inheritance: classes, edits, copies | Composition: classes, edits, copies | Cheaper |
|---|---|---|---|
| Add a Goose | 4, 0, 0 | 7, 0, 0 | inheritance |
| Add a Penguin | 5, 3, 0, and one crash before the fix | 8, 0, 0 | composition |
| Robot birds (after a Goose) | 7 (+3), 0, 2 | 8 (+1), 0, 0 | composition |
| A duck breaks its wing | 3, 1, 0 | 7, 0, 0 | composition |
| All four changes | 10, 5, 3 | 10, 0, 0 | composition |
In the last demo both designs end with 10 classes, but inheritance got there by editing working classes 5
times and copying the rocket code 3 times, and its robot penguin still can't join the
List<FlyingBird>.
When to pick which
| Situation | Pick | Why |
|---|---|---|
| The child really is a parent, everywhere the parent is used | inheritance | least code; the parent's methods come for free |
| A framework hands you a base class to extend (a servlet, a test case, an Android activity) | inheritance | that is how the framework calls your code |
| Some subclasses can't do what the parent promises | composition | no method has to throw; Liskov holds |
| Behaviour varies along two or more independent axes | composition | parts add up, subclasses multiply |
| Behaviour must change while the program runs | composition | an object can swap a field, not its class |
| The parent class belongs to someone else and may change | composition | a subclass depends on the parent's insides (the fragile base class problem) |
The Design Patterns book (1994) put it as "favor object composition over class inheritance". Favor is not never: inheritance is still the right tool for a true, stable is-a.
Common mistakes
- Modelling the real world instead of the behaviour. "A penguin is a bird" is true in biology; in code, is-a means "can be used everywhere a Bird is used".
- Overriding a method to throw or do nothing. It hides the problem until some caller relies on the promise.
- Inheriting only to reuse a helper method. If the child isn't a parent, hold a helper object instead.
- Composition everywhere. A class made of five one-line parts with forwarding methods is harder to read than a small, honest hierarchy.
What the page leaves out
The page uses Java's single class inheritance. It leaves out interfaces with default methods, multiple inheritance and the diamond problem (C++, Python), mixins and traits (Scala, Rust, PHP), and languages built around composition such as Go's struct embedding. It also leaves out the fragile base class problem in detail, abstract classes that are meant to be extended, dependency injection frameworks that do the wiring, and the cost of the extra delegation call, which a JIT compiler usually inlines away.