2.1.15 · D4OOP Fundamentals

Exercises — Composition — has-a relationship vs is-a

2,555 words12 min readBack to topic

Before we start, one picture to keep in your head the whole time — the two arrows that define this entire chapter.

Figure — Composition — has-a relationship vs is-a

The hollow arrow points from the child up to the kind it is (inheritance, "is-a"). The diamond arrow points from the whole to a part it holds (composition, "has-a"). Every exercise below asks you to choose one of these two arrows.


Level 1 — Recognition

Goal: given a sentence, pick has-a or is-a. No code yet — just the English test from the parent.

Recall Solution 1.1

Read each sentence aloud and keep whichever is grammatically true.

  1. "A playlist has songs" ✅ → has-a (composition). "A playlist is a song" ❌.
  2. "A circle is a shape" ✅ → is-a (inheritance).
  3. "A laptop has a battery" ✅ → has-a (composition).
  4. "A savings account is a bank account" ✅ → is-a (inheritance) — and substitutability holds: anywhere you use a BankAccount, a SavingsAccount works.
  5. "A book has an author" ✅ → has-a (composition). "A book is an author" ❌.

Score: 5/5 correct = you own the English test.

Recall Solution 1.2

False. Storing a field is necessary but not sufficient. Composition specifically means you delegate behaviour to a held object that models a real part. A String name field is just data — you don't call methods on it to do the class's job. An Engine engine field is composition because Car.start() forwards work to engine.ignite(). Rule of thumb: ask "do I call this object's methods to get my job done?" If yes → composition. If it's just a value you read → plain data.


Level 2 — Application

Goal: turn a chosen relationship into working code — a hidden field plus delegating methods.

Recall Solution 2.1
class InkCartridge:
    def __init__(self, units=3):
        self.units = units
    def page(self):
        if self.units > 0:
            self.units -= 1
            return "printed"
        return "out of ink"
 
class Printer:
    def __init__(self, cartridge):     # injected in — dependency injection
        self._cartridge = cartridge    # the has-a field (hidden with _)
    def print_page(self):
        return self._cartridge.page()  # delegation
 
p = Printer(InkCartridge(3))
print(p.print_page())  # printed
print(p.print_page())  # printed
print(p.print_page())  # printed
print(p.print_page())  # out of ink

Why hidden (_cartridge)? So outside code can't reach past the printer and reset ink directly — that keeps encapsulation intact. Why inject the cartridge? So you can hand in a full, empty, or fake cartridge for testing without editing Printer (Dependency Injection).

Recall Solution 2.2
class Queue:
    def __init__(self):
        self._items = []            # has-a list, hidden
    def enqueue(self, x): self._items.append(x)
    def dequeue(self):    return self._items.pop(0)
    def size(self):       return len(self._items)

Dangerous exposure in the bad version: because Queue(list) inherits the entire list interface, a user can call q.insert(2, x) or q[0] = y and jam an item into the middle, destroying the FIFO (first-in-first-out) guarantee. This is the same trap as Stack(list) in the parent note — an over-strong is-a claim leaks a surface you never wanted. See Aggregation vs Composition (UML) for how this "owned part" is drawn.


Level 3 — Analysis

Goal: explain WHY a design breaks or holds, not just what it does.

Recall Solution 3.1

Two breakages:

  1. t.clear() — inherited from dict, wipes "t0" and any state the timer relied on.
  2. t["t0"] = "banana" or t.update({"t0": -99}) — a caller can set the internal clock to nonsense; Timer has no way to guard it.

Principle violated: the Liskov Substitution Principle is being abusedTimer claims "is a dict" but a timer is not substitutable for a dict (you'd never pass a timer where arbitrary key-value storage is expected). Because the is-a claim is false, the inherited interface leaks control. This is the Fragile Base Class Problem in miniature: the timer's correctness depends on internals the base class exposes. Fix: Timer should have-a dict (self._state = {}) and expose only start()/elapsed().

Recall Solution 3.2

Design B couples more loosely.

  • Design A exposes and depends on all 8 of Engine's public methods plus whatever Engine adds in future — the surface Car is glued to = 8 (and growing).
  • Design B depends on exactly 1 method, ignite(). Surface = 1.

Loose coupling = smaller promised surface = fewer things that can break Car when Engine changes. Design B's contract is 1 method vs 8 — an 8× smaller blast radius. This is precisely why the parent says favor composition over inheritance.


Level 4 — Synthesis

Goal: combine multiple parts to defeat the combinatorial class explosion.

Recall Solution 4.1
class Walk: def name(self): return "walk"
class Fly:  def name(self): return "fly"
class Cast: def name(self): return "cast"
 
class Character:
    def __init__(self, abilities):     # has-a list of part-objects
        self._abilities = abilities
    def act(self):
        return [a.name() for a in self._abilities]
 
mage_walker = Character([Walk(), Cast()])
print(mage_walker.act())   # ['walk', 'cast']

Why this beats 8 classes: each ability is a part you plug in, so the number of possible characters is combinations of parts (mixed at runtime), not a fixed tower of subclasses (fixed at compile time). Adding a 4th ability (Swim) costs one small class, not doubling the hierarchy to 16. This is the Strategy Pattern: behaviour is an interchangeable object. See the figure — one whole, many pluggable parts.

Figure — Composition — has-a relationship vs is-a
Recall Solution 4.2
hero = Character([Walk(), Cast()])
print(hero.act())               # ['walk', 'cast']
hero._abilities[0] = Fly()      # swap the part at runtime
print(hero.act())               # ['fly', 'cast']

Why inheritance can't: a class's base classes are fixed at compile/definition time — an object cannot change what it is a kind of while running. But an object can change what parts it holds any moment. Runtime swapping is the signature superpower of composition (the parent's car.engine = ElectricEngine()).


Level 5 — Mastery

Goal: senior-level judgement — choose the right tool under real constraints and defend it.

Recall Solution 5.1

Compose the policy as an injected part:

class NoDiscount:
    def apply(self, subtotal): return subtotal
class PercentOff:
    def __init__(self, pct): self.pct = pct
    def apply(self, subtotal): return subtotal * (1 - self.pct/100)
 
class Order:
    def __init__(self, subtotal, policy):   # policy injected (has-a)
        self.subtotal = subtotal
        self._policy = policy
    def total(self):
        return self._policy.apply(self.subtotal)   # delegation
 
o = Order(200, PercentOff(10))
print(o.total())   # 180.0

Concrete number: subtotal with a policy → . Two principles:

  1. Favor composition over inheritance — swap the campaign at runtime (o._policy = NoDiscount()) without new order subclasses (this is the Strategy Pattern again, delivered via Dependency Injection).
  2. Liskov holds trivially — every policy honours the same one-method contract apply(subtotal), so orders never break when a new policy appears.
Recall Solution 5.2

Inheritance is correct for the identity axis: "a manager is an employee" is genuinely true — a Manager is fully substitutable anywhere an Employee is expected (payroll, id lookup). So class Manager(Employee) respects Liskov. Blindly banning inheritance would force awkward delegation of every employee method for zero benefit. Composition belongs on the behaviour axis: the team-leading responsibility is a part the manager hasself._team = Team([...]). Leadership strategy (round-robin reviews vs pairwise) can then be swapped without touching the Employee identity. Master's rule: use is-a inheritance for genuine type identity, has-a composition for pluggable behaviour. They are partners, not rivals — see Inheritance — is-a relationship for the identity side.


Recall Self-test checklist (reveal to grade yourself)

Can you do all of these without notes? Turn any English sentence into is-a or has-a ::: L1 — apply "A is a B" vs "A has a B". Write a hidden field + delegating method from scratch ::: L2 — see Printer/Queue. Explain why a bad is-a leaks a dangerous surface ::: L3 — count the exposed interface. Defeat the class explosion with pluggable parts ::: L4 — abilities as objects in a list. Choose inheritance vs composition under real constraints and defend it ::: L5 — identity → inherit, behaviour → compose.

Connections

  • 2.1.15 Composition — has-a relationship vs is-a (Hinglish)
  • Inheritance — is-a relationship
  • Liskov Substitution Principle
  • Encapsulation and Information Hiding
  • Dependency Injection
  • Strategy Pattern
  • Aggregation vs Composition (UML)
  • Fragile Base Class Problem