A practical guide to writing flexible, maintainable code
Introduction
Every object-oriented developer eventually faces the same dilemma: two classes share some behaviour, and the natural instinct is to create a parent class and have them both inherit from it. It feels clean. It feels right. But more often than not, a few months down the road, that hierarchy starts to crack under the weight of new requirements.
This article explores why favour composition over inheritance — one of the most important principles in software design — what it means in practice, and how to apply it in your everyday code.
What Is Inheritance?
Inheritance is a mechanism where one class (the child or subclass) derives behaviour and state from another class (the parent or superclass). The child extends the parent, inheriting its methods and fields, and can override or add to them.
A classic textbook example:
class Animal:
def speak(self):
raise NotImplementedError
class Dog(Animal):
def speak(self):
return "Woof!"
class Cat(Animal):
def speak(self):
return "Meow!"
This looks perfectly fine. And for simple taxonomies, it often is. The problem appears when real-world requirements stop behaving like a tidy class diagram.
The Problems with Deep Hierarchies
The Fragile Base Class Problem
When you change a base class, you risk breaking every subclass. Because subclasses depend on the internal implementation of the parent — not just its public interface — even a small refactor can have cascading, unexpected side effects.
The Gorilla–Banana Problem
"You wanted a banana but what you got was a gorilla holding the banana and the entire jungle." — Joe Armstrong
Inheritance forces you to take everything the parent offers, whether you need it or not. If a subclass only needs one method from a parent that has twenty, it still inherits all the state and behaviour — along with all the associated complexity and risk.
The Diamond Problem
What happens when a class needs to inherit behaviour from two different parents that both define the same method? Most languages either forbid this entirely or introduce complicated resolution rules. It is a symptom of a deeper design flaw.
Violation of Encapsulation
Subclasses are intimately coupled to their parents. They often need to know about protected internals, call super() in the right order, and respect implicit contracts that are never written down. This tight coupling makes code hard to understand and refactor independently.
What Is Composition?
Composition means building complex behaviour by combining simpler, focused objects — rather than by extending a class hierarchy. Instead of asking "what is this object?", you ask "what does this object do?"
Where inheritance models an is-a relationship, composition models a has-a relationship.
| Approach | Relationship | Example |
|---|---|---|
| Inheritance | is-a |
Dog is an Animal
|
| Composition | has-a |
Robot has a Speaker
|
A Side-by-Side Comparison
Imagine you are building a game with different types of characters. Some can fly, some can swim, some can fight — and some can do combinations of all three.
The Inheritance Approach (and its problems)
class Character:
pass
class FlyingCharacter(Character):
def fly(self): ...
class SwimmingCharacter(Character):
def swim(self): ...
# What about a character that can BOTH fly and swim?
# Most languages won't allow this cleanly.
class FlyingSwimmingCharacter(FlyingCharacter, SwimmingCharacter):
pass # Multiple inheritance — this gets messy fast
Every new combination of abilities requires a new class. The hierarchy explodes in complexity.
The Composition Approach
class FlyBehaviour:
def fly(self):
print("Soaring through the sky!")
class SwimBehaviour:
def swim(self):
print("Gliding through the water!")
class FightBehaviour:
def fight(self):
print("Attacking!")
class Character:
def __init__(self, behaviours):
self.behaviours = behaviours
def perform(self, action):
behaviour = self.behaviours.get(action)
if behaviour:
getattr(behaviour, action)()
# Build any combination at will
hero = Character({
"fly": FlyBehaviour(),
"swim": SwimBehaviour(),
"fight": FightBehaviour(),
})
hero.perform("fly") # Soaring through the sky!
hero.perform("swim") # Gliding through the water!
Adding a new ability now means writing one new class — not reshaping the entire hierarchy.
Composition in the Real World
Strategy Pattern
The Strategy pattern is composition in its most recognisable form. You define a family of algorithms, encapsulate each one, and make them interchangeable.
class JSONSerializer:
def serialize(self, data):
import json
return json.dumps(data)
class XMLSerializer:
def serialize(self, data):
# ... XML serialization logic
return f"<data>{data}</data>"
class DataExporter:
def __init__(self, serializer):
self.serializer = serializer # composed, not inherited
def export(self, data):
return self.serializer.serialize(data)
exporter = DataExporter(JSONSerializer())
exporter.export({"name": "Alice"})
# Swap the strategy without touching DataExporter
exporter.serializer = XMLSerializer()
exporter.export({"name": "Alice"})
Dependency Injection
Dependency injection is composition by another name. Rather than a class constructing its own dependencies (or inheriting them), they are passed in from the outside. This keeps classes loosely coupled and trivially testable.
class EmailNotifier:
def notify(self, message):
print(f"Sending email: {message}")
class SMSNotifier:
def notify(self, message):
print(f"Sending SMS: {message}")
class OrderService:
def __init__(self, notifier):
self.notifier = notifier # injected dependency
def place_order(self, order):
# ... process order
self.notifier.notify(f"Order {order.id} confirmed.")
# In tests, swap for a mock — no subclassing needed
service = OrderService(EmailNotifier())
When Is Inheritance Actually Appropriate?
Composition over inheritance does not mean never use inheritance. Inheritance is a valid and useful tool when the relationship genuinely satisfies all of the following:
- The is-a relationship is stable and true across all use cases —
Squareis aShape,HTTPExceptionis anException. - You want to share a genuine, semantic contract, not just re-use some code.
- The hierarchy is shallow — ideally no more than two levels deep.
- You are extending a framework or library that is specifically designed to be subclassed (e.g. Django's
Viewclasses, Python'sunittest.TestCase).
A useful heuristic: if you would feel awkward saying "X is a kind of Y" out loud to a non-programmer, reach for composition instead.
The Liskov Substitution Principle
The Liskov Substitution Principle (LSP) — the L in SOLID — states that objects of a subclass should be substitutable for objects of their superclass without altering the correctness of the program.
The classic violation:
class Rectangle:
def set_width(self, w): self.width = w
def set_height(self, h): self.height = h
def area(self): return self.width * self.height
class Square(Rectangle):
def set_width(self, w):
self.width = w
self.height = w # A square must keep sides equal
def set_height(self, h):
self.width = h # This breaks Rectangle's contract!
self.height = h
A Square seems like it should be a Rectangle, but the subclass breaks the parent's behavioural contract. Any code that relies on being able to set width and height independently will malfunction when handed a Square.
If your inheritance violates LSP, it is a strong signal that you need composition instead.
Summary
| Inheritance | Composition | |
|---|---|---|
| Relationship | is-a | has-a |
| Coupling | Tight (shares internals) | Loose (uses interface) |
| Flexibility | Low (hierarchy is fixed) | High (swap behaviours at runtime) |
| Reuse unit | Entire class | Individual behaviours |
| Testability | Harder (must instantiate hierarchy) | Easier (inject mocks) |
| Risk of change | High (fragile base class) | Low (changes are localised) |
Inheritance is a powerful tool, but it is one that is easy to misuse. The moment you find yourself creating a subclass primarily to re-use some code — rather than to express a genuine taxonomic relationship — it is time to reach for composition.
Prefer composition. Build your objects from small, focused, interchangeable pieces. Your future self, and everyone else who reads the code, will thank you.
Top comments (0)