Course contents

Composition vs inheritance

Sign in to run this lab and save your progress

Learn

Before you start

This lesson teaches a habit that will matter more than any single design pattern: prefer composition over inheritance.

The mindset shift

When you first learn inheritance, everything looks like a tree. "Duck is a Bird. Fish is an Animal. Car is a Vehicle."

Then you meet a Fish that can't fly, a rubber duck that doesn't eat, or a self-driving Car with no driver. The tree starts growing exceptions. Every new type either creates a new branch or breaks an existing one.

Composition changes how you think:

  • Instead of asking what IS this, ask what does it HAVE.
  • A Duck HAS the ability to fly, swim, and eat.
  • A rubber duck HAS the ability to eat that raises "not edible", plus the same fly and swim.
  • A robot fish HAS the ability to swim but no ability to eat.

Each ability is its own small class. The main object holds a few of them and hands off the work.

Why the industry moved this way

Every serious book about OOP from the last 20+ years says the same thing:

  • Design Patterns (Gang of Four, 1994) — italicized rule: "Favor object composition over class inheritance."
  • Effective Java (Bloch) — Item 18: "Favor composition over inheritance."
  • Modern languages like Go and Rust don't even have inheritance. They use composition + interfaces instead.

Not a coincidence. Composition scales better as codebases grow. Deep inheritance trees are one of the top three sources of "we can't change this without breaking everything" in real production code.

The interview payoff

In an interview, when the interviewer adds a new requirement ("what if some parking spots are electric?"), watch what happens:

  • Inheritance-first person: needs to add a new subclass, maybe change the parent, hopes nothing else breaks.
  • Composition-first person: adds a charging_capability field to the spot. Done.

The composition answer is faster, cleaner to read, and doesn't cause bugs in other classes. That's what interviewers really watch for.

About the exercise

Notifications are the textbook composition problem. Every real notification system (Twilio, AWS SNS, your company's) uses this shape: a small stable core, plug-in channel classes, no inheritance between channel types.

One of the tests adds a Slack channel with ZERO changes to existing code. That's the composition win, made concrete. Feel it in your fingers today; you'll reach for it in every design problem from here on.

Your task

Inheritance is easy to reach for and easy to misuse. This lesson teaches the tool most senior engineers actually reach for first: composition.

You'll hear this rule a lot in your career: prefer composition over inheritance.

A classic bad example

Say you're building an animal app. You start with:

class Animal:
    def eat(self): print("nom nom")
    def fly(self): print("flap flap")
    def swim(self): print("splash splash")

class Duck(Animal): pass
class Fish(Animal): pass    # can't fly — but inherits fly() anyway
class Sparrow(Animal): pass # can't swim — but inherits swim() anyway

Every animal now has fly() and swim() methods, even the ones that shouldn't. You can override them to raise an error, but now the CALLERS have to guess which animals can actually do which things. The parent class is lying about what its children can do.

The composition fix

Instead of inheritance, give an Animal its abilities as separate objects it holds:

class FlyStrategy:
    def fly(self): print("flap flap")

class NoFlyStrategy:
    def fly(self): raise ValueError("this animal can't fly")

class Animal:
    def __init__(self, name, fly_strategy, swim_strategy):
        self.name = name
        self._fly = fly_strategy
        self._swim = swim_strategy

    def fly(self): self._fly.fly()
    def swim(self): self._swim.swim()

Now building an animal is one line:

duck = Animal("duck", FlyStrategy(), SwimStrategy())
fish = Animal("fish", NoFlyStrategy(), SwimStrategy())
sparrow = Animal("sparrow", FlyStrategy(), NoSwimStrategy())

Each animal only has the abilities you gave it. Adding a new ability (like sing) means adding a new parameter for the animals that sing — no other class has to deal with it.

This is a preview of the Strategy pattern (module 3).

When to use which

Inheritance works when:

  • The child is really a special kind of the parent.
  • Every child makes sense using every parent method.
  • You only need to override one or two methods.

Composition works when:

  • You'd have to override lots of parent methods to say "actually, this doesn't apply to me".
  • You want to mix and match abilities (a winged fish? a swimming bird?).
  • You want to change behavior while the program is running.

Most design problems end up with composition once you look at them for more than 30 seconds.

Your task

You're building a small notification system. Users get notifications through different channels (email, SMS, push). Some users want all channels, some want only push, some want nothing.

Build it with composition, not inheritance:

  1. Three channel classes: EmailChannel, SmsChannel, PushChannel. Each has a send(user_id, message) method that returns a string like "email to user_id: message".
  2. A NotificationService class:
    • Takes a list of channels when you create it, and stores it privately as self._channels (leading underscore — encapsulation from lesson 2 still applies). Callers should have no reason to reach in.
    • Has a notify(user_id, message) method that calls send on EACH channel in order and returns the list of results.
  3. The three channel classes do NOT share a parent. Each is on its own.

The tests build users with different channel mixes and check that notify() returns the expected list. Click Submit to run them.

Starter files

Preview only — open the lab to edit and run.

files_dir
04-composition-vs-inheritance-starter