Skip to Content
Course content

135: Composition Over Inheritance

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Listen, by now you're probably comfortable with inheritance. You've built base classes, overridden methods, and you feel like you've got a handle on the "is-a" relationship. But here is a secret from the trenches: in professional production code, we actually try to avoid deep inheritance hierarchies whenever possible. We prefer composition.

Think of it like building a custom PC. If you wanted a "Gaming PC," you wouldn't create a blueprint for a "Basic PC" and then create a subclass called "Gaming PC" that inherits everything from the basic one. Why? Because that's too rigid. If you suddenly decide your Gaming PC needs a different power supply or a specific liquid cooling system, you're stuck fighting the structure of the parent class. Instead, you treat a PC as a container. You buy a case, then you plug in a GPU, a CPU, and some RAM. The PC "has-a" GPU; it isn't "a type of" GPU. If the GPU breaks or you want an upgrade, you just swap that one component without redesigning the entire machine.

The Trap of the "Is-A" Relationship

In Python, we often start with inheritance because it feels intuitive. "A Warrior is a Character, so I'll inherit from Character." That works great until you need a BattleMage. Now you're stuck. Do you inherit from both Warrior and Mage? That leads to the "Diamond Problem" and a nightmare of method resolution orders (MRO) that will make you want to quit coding for the week.

# The "Inheritance Trap" way
class Character:
    def move(self):
        print("Moving...")

class Warrior(Character):
    def attack(self):
        print("Swings a sword!")

class Mage(Character):
    def attack(self):
        print("Casts a fireball!")

# Now what? A BattleMage needs both. Multiple inheritance is messy.
class BattleMage(Warrior, Mage): 
    pass

The problem here is that we've tied the identity of the object (what it is) to its behavior (what it can do). That's a recipe for fragile code.

Building with LEGOs instead of Casts

Composition flips the script. Instead of saying a BattleMage is a Warrior and a Mage, we say a Character has a combat style. We move the behavior into its own separate class and plug it into our main object. This is the "has-a" relationship.

class SwordAttack:
    def execute(self):
        print("Swings a sword!")

class SpellAttack:
    def execute(self):
        print("Casts a fireball!")

class Character:
    def __init__(self, name, attack_style):
        self.name = name
        # This is composition. We store an instance of another class.
        self.attack_style = attack_style

    def perform_attack(self):
        print(f"{self.name} attacks:", end=" ")
        self.attack_style.execute()

# Now we can create any combination we want without complex hierarchies
warrior = Character("Grog", SwordAttack())
mage = Character("Zelda", SpellAttack())
battle_mage = Character("Valerius", SpellAttack()) # Start as mage...

# And here is the magic: we can change behavior at runtime!
battle_mage.attack_style = SwordAttack() # ...then switch to sword!

By decoupling the attack logic from the Character class, I've made the code infinitely more flexible. I don't need a new class for every single combination of abilities; I just mix and match components.

When to actually stick with inheritance

I'm not telling you to delete every (BaseClass) in your code. Inheritance is still the right tool when you have a true, unchanging taxonomic relationship. If you're building a system for biological species, a Dog will always be a Mammal. That's not a "component" you swap out on a Tuesday afternoon.

But as a rule of thumb: if you find yourself creating subclasses just to reuse a few methods, or if you're struggling with "Multiple Inheritance," stop. Step back. Ask yourself: "Should this object be this thing, or should it just have this thing?" Nine times out of ten, composition is the cleaner path.




📋 Practical Task

Building a Modular Smart-Home Device

You are designing a smart-home system. Instead of creating a rigid inheritance tree (e.g., SmartLight, SmartThermostat), you need to build a SmartDevice class that uses composition to handle different functionalities.

Requirements:

  • Create two "Component" classes: LightingComponent (with a method operate() that prints "Adjusting brightness...") and ClimateComponent (with a method operate() that prints "Adjusting temperature...").
  • Create a SmartDevice class. It should take a name and a component in its __init__ method.
  • The SmartDevice should have a method called activate() that calls the operate() method of whatever component it currently holds.
  • In your main execution block:
    1. Create a device named "Living Room Hub" with a LightingComponent.
    2. Call activate() on it.
    3. Swap the component of the "Living Room Hub" to a ClimateComponent.
    4. Call activate() again to prove the behavior has changed without creating a new object.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.