Composition vs Inheritance: Why 'Has-a' Often Beats 'Is-a'
Inheritance is like a family tree. A child inherits traits from their parents, grandparents, and beyond. It's powerful, but sometimes the family tree gets tangled and confusing.
Composition is like building with LEGO blocks. Instead of inheriting behavior from a parent, you snap together small, independent pieces. Each piece does one thing well, and you combine them however you want.
In this tutorial, you'll learn why experienced developers often prefer composition over inheritance, how to spot when inheritance is the wrong choice, and how to refactor your code to use composition instead.
What Is the "Is-a" Relationship in Inheritance?
Inheritance models an "is-a" relationship. A Dog "is an" Animal. A Car "is a" Vehicle. The child class IS a more specific version of the parent class.
This works perfectly. A Dog really IS an Animal, and the behavior makes sense. Inheritance shines when there's a genuine type hierarchy.
What Is Composition and How Does "Has-a" Work?
Composition models a "has-a" relationship. A Car "has an" Engine. A Computer "has a" CPU. Instead of inheriting behavior, the object contains another object that provides the behavior.
Notice that Car doesn't inherit from Engine. Instead, it stores an Engine object and delegates work to it. The car uses the engine, but they're separate, independent pieces.
The beauty of composition is flexibility. You can easily swap out the engine without changing the Car class at all.
When Does Inheritance Become a Problem?
Inheritance starts causing trouble when you try to share behavior that doesn't follow a natural type hierarchy. Here's a classic example that goes wrong.
This might look fine with two abilities, but imagine adding running, climbing, digging, teleporting, and more. The number of possible combinations explodes, and you end up with a messy web of classes.
# Need a class for every combination!
class FlyingSwimming(Character): ...
class FlyingRunning(Character): ...
class SwimmingRunning(Character): ...
class FlyingSwimmingRunning(Character): ...
# This doesn't scale!# Mix and match abilities freely
duck = Character('Duck',
abilities=[Fly(), Swim()])
penguin = Character('Penguin',
abilities=[Swim()])
dragon = Character('Dragon',
abilities=[Fly(), Swim(), Breathe_Fire()])How Do You Refactor Inheritance Into Composition?
Let's fix the game character example using composition. Instead of inheriting abilities, each character will HAVE a list of ability objects.
Now adding a new ability is easy — just write a new ability class. No need to change any existing code. And you can give any combination of abilities to any character.
What Is Dependency Injection and Why Does It Matter?
Dependency injection means passing objects to a class from the outside instead of creating them inside. This is composition in action — you "inject" the pieces your object needs.
The Report class doesn't know or care what kind of formatter it gets. It just calls formatter.format(). This makes the code easy to test and easy to extend.
Dependency injection is especially powerful for testing. You can inject a "fake" object that records what happens instead of doing real work.
Practice Exercises
Create a class CPU with a method process() that returns 'Processing data...'. Create a class Computer whose __init__ takes a cpu parameter and stores it as self.cpu. Add a method run() that returns the result of self.cpu.process(). Create a Computer with a CPU and print the result of run().
Read the code carefully. What will be printed? Pay attention to which logger is used in each case.
Create two ability classes: Swim with a method action() returning 'swimming', and Climb with a method action() returning 'climbing'. Create a Character class that takes name (str) and abilities (list). Add a method show_abilities() that prints '{name} can: ' followed by each ability's action joined by ', '. Create a Character('Bear', [Swim(), Climb()]) and call show_abilities().
This code uses inheritance, but Robot is NOT a Speaker — it HAS a speaker. Refactor so that Speaker is a separate class with a speak(text) method returning the text string. Robot should take a name and a Speaker in its constructor, storing the speaker. Its say(text) method should print the result of self.speaker.speak(text). Create a Robot('R2D2', Speaker()) and call say('Hello').
Create two notifier classes: EmailNotifier with a method send(msg) that returns 'Email: ' + msg, and SMSNotifier with send(msg) returning 'SMS: ' + msg. Create an AlertSystem class that takes a notifier in its constructor. Add a method alert(msg) that prints the result of self.notifier.send(msg). Create two AlertSystems (one with each notifier) and call alert('Server down') on each.