software-design/composition-vs-inheritance-why-a-toy-duck-shouldn-t-learn-to-fly.md

Composition vs inheritance: why a toy duck shouldn't learn to fly

Building software · 5 min read ·

A rubber duck that learns to fly is a classic sign that inheritance, the "is a" way of sharing code, has been stretched too far. Every behaviour you add can double the number of classes you need, and Python's multiple inheritance can silently hide one behaviour behind another.

Build a creature from abilities in the page, watch a grid of required classes double as you add one more ability, and compare it with *composition, where a creature simply has* small parts. Includes three Python examples you can run, and a plain rule for when inheritance is still the right tool.

Sixteen grey boxes, one highlighted, versus four labelled ability parts feeding a single Creature box

A duck, a toy duck, and a bug

Imagine you are building a small game full of creatures. You write a Duck that can swim and fly. Later you add a RubberDuck, because a rubber duck is a kind of duck, right? Then someone reports that the rubber duck takes off. This article is about why that happens, and about a different way of sharing code that avoids it.

Two terms first. A class is a blueprint for making objects: it lists what an object knows and what it can do. Inheritance lets one class (the subclass) automatically receive everything from another (the parent class), so you do not copy and paste. It is the first tool most people learn for sharing code, and it is very good at one thing and surprisingly bad at another.

Where inheritance bites

Here is the duck family in Python. RubberDuck has an empty body (pass), so it receives everything Duck has:

class Duck:
    def swim(self):
        return "paddles"
    def fly(self):
        return "flaps into the air"

class RubberDuck(Duck):
    pass

toy = RubberDuck()
print(toy.swim())
print(toy.fly())

Running it prints:

paddles
flaps into the air

The toy flies, because inheritance hands over everything, whether you wanted it or not. You can patch it by overriding fly in RubberDuck, but now the subclass spends its life cancelling things its parent promised. Each new creature adds more cancelling, and the parent class becomes a promise nobody keeps.

The combination problem

The bigger trouble shows up when behaviours mix. Say your monsters can fly, swim, shoot fireballs or burrow, in any combination. Using inheritance you would create a class for each combination you need: FlySwimMonster, FlyShootMonster, FlySwimShootMonster, and so on. With 2 abilities there are 4 combinations (including "none"), with 4 there are 16, with 6 there are 64. Each ability you add doubles the number of possible classes.

Python lets a class inherit from two parents (called multiple inheritance), which looks like an escape. It comes with its own surprise:

class Monster:
    def act(self):
        return "stands still"

class FlyingMonster(Monster):
    def act(self):
        return "flies"

class SwimmingMonster(Monster):
    def act(self):
        return "swims"

class FlySwimMonster(FlyingMonster, SwimmingMonster):
    pass

print(FlySwimMonster().act())
print([c.__name__ for c in FlySwimMonster.__mro__])
flies
['FlySwimMonster', 'FlyingMonster', 'SwimmingMonster', 'Monster', 'object']

The monster only flies. Both parents define act, so Python picks one by walking a fixed search order, the method resolution order (MRO), printed on the second line. FlyingMonster comes first and wins; SwimmingMonster.act is never reached. The swimming did not combine, it was hidden.

Composition: build from parts

Composition flips the question. Instead of asking "what is this thing?" it asks "what does this thing have?". A creature is one ordinary class that holds a list of small, separate ability objects and asks each of them to act:

class Fly:
    def act(self):
        return "flies"

class Swim:
    def act(self):
        return "swims"

class Creature:
    def __init__(self, name, abilities):
        self.name = name
        self.abilities = abilities

    def act(self):
        return [f"{self.name} {a.act()}" for a in self.abilities]

imp = Creature("Imp", [Fly(), Swim()])
for line in imp.act():
    print(line)

rock = Creature("Rock", [])
print(rock.act())
Imp flies
Imp swims
[]

The imp flies and swims, nothing is hidden, and the rock, which has no abilities, simply does nothing. You never wrote an ImpClass. To get a new kind of creature you list different parts. To add a new ability you write one small class, and every creature can use it straight away.

Try it: count the classes

So is inheritance bad?

No. Inheritance is a good fit when the "is a" statement is true in every way that matters, and the family stays small and shallow. A ValueError really is an Exception, and Python's own error classes are built that way. Trouble starts when you inherit only to reuse code rather than because the relationship is true, or when the tree keeps growing sideways with each new feature.

A well-known piece of advice, "favour object composition over class inheritance", comes from the 1994 book Design Patterns by Gamma, Helm, Johnson and Vlissides. Read it as a lean, not a law: reach for composition first, and use inheritance when it is honestly a true "is a".

What to do in your own code

When you are about to write class B(A), ask two quick questions. Would every method of A make sense on a B, with no cancelling? And can you picture a second, independent way B might need to vary, such as the swimming and the flying above? If the answer to the first is no, or to the second is yes, store a part instead: give your class a field that holds a small object and ask that object to do the work. Parts are easier to test on their own, easy to swap, and they never double the size of your class list.

$ ls software-design/

see all →
software-design/the-observer-pattern-how-a-shop-tells-many-functions-about-one-order-w.md The observer pattern: how a shop tells many functions about one order without knowing them A shop that must email a receipt, update stock and add loyalty points ends up with one function that knows about everything. Every new… 5 min read · 27 Oct software-design/refactoring-how-to-tidy-working-code-without-breaking-it.md Refactoring: how to tidy working code without breaking it Refactoring means changing how code is built on the inside without changing what it does. It sounds risky, but there is a simple routine… 6 min read · 24 Oct software-design/unit-tests-why-four-passing-tests-don-t-prove-a-function-works.md Unit tests: why four passing tests don't prove a function works Your function gave the right answer four times. Is it correct? Not necessarily: passing tests only show the bug isn't where you looked… 4 min read · 21 Oct software-design/idempotency-why-clicking-pay-twice-should-charge-you-once.md Idempotency: why clicking Pay twice should charge you once You tap Pay, the screen shows an error, you tap again, and your card is charged twice. Networks drop messages, so a phone often cannot tell… 6 min read · 18 Oct software-design/state-machines-why-an-order-can-t-be-delivered-before-it-s-paid.md State machines: why an order can't be delivered before it's paid An online order can be in the cart, paid, shipped, delivered or cancelled, but what stops the code from delivering an order nobody paid… 4 min read · 15 Oct software-design/dependency-injection-why-a-test-passes-at-9am-and-fails-at-3pm.md Dependency injection: why a test passes at 9am and fails at 3pm A test that passes in the morning and fails in the afternoon, with no change to the code, is a puzzle many beginners meet. The cause is… 5 min read · 12 Oct