Object-Oriented Programming~20 min

Composition

Build an object out of other objects, each responsible for its own part, and let method calls delegate instead of duplicating logic.

By the end of this lesson, you can

  • Explain composition as building an object out of other objects, each responsible for its own part
  • Trace how a method call on a composed object delegates work to the object it contains
  • Explain why composition models a "has-a" relationship more naturally than duplicating logic would

Why it matters

A library member has an account for tracking fines — that’s a relationship between two separate things, not one thing wearing two hats. Reimplementing balance-tracking logic directly inside a Member class would duplicate everything Account already does, including every rule it enforces. Composition avoids that by having Member hold an Account, and delegate to it.

Mental model

Composition means an object holds another object as one of its attributes, and calls that object’s methods to get work done — rather than reimplementing that behavior itself. It models a “has-a” relationship directly: a member has an account.

PythonMember is composed of an Account
class Account:
    def __init__(self, starting_balance):
        self._balance = starting_balance

    def withdraw(self, amount):
        if amount > self._balance:
            return False
        self._balance = self._balance - amount
        return True

    def balance(self):
        return self._balance


class Member:
    def __init__(self, name, starting_balance):
        self.name = name
        self.account = Account(starting_balance)

    def pay_fine(self, amount):
        return self.account.withdraw(amount)


member = Member("Ada", 20)
print(member.pay_fine(5))
print(member.account.balance())
Output
True
15

Member.__init__ creates and holds an Account as self.account. pay_fine doesn’t touch any balance logic itself — it calls self.account.withdraw(amount) and returns whatever that reports. Member doesn’t need to know how Account enforces its rules, only that it does.

Trace it

pay_fine delegates every call to the composed Account
CallDelegates toaccount.balance() afterward
Member("Ada", 20)Account(20) is created and held as self.account20
member.pay_fine(5)self.account.withdraw(5)15
member.pay_fine(100)self.account.withdraw(100) — refused, exceeds balance15 (unchanged)

Every rule Account.withdraw enforces — refusing to overdraw — applies automatically to every Member, without Member ever mentioning that rule itself.

Check your understanding

What does this print? member = Member("Grace", 10) print(member.pay_fine(3)) print(member.pay_fine(3)) print(member.account.balance())
Or reveal the answer without checking

Answer:True, True, 4
Each pay_fine(3) delegates to account.withdraw(3), which succeeds both times (10 -> 7 -> 4), since 3 never exceeds the current balance at either point.

Why does composing an Account inside Member work better than copying Account's balance logic directly into Member?
Or reveal the answer without checking

Answer:Every rule Account enforces applies automatically through delegation, and only needs to be maintained in one place
Delegating to a composed Account means Member automatically benefits from every rule Account enforces, now and in the future, without duplicating any of that logic itself.

Practice: warm-up

Trace this program — write down member.account.balance() after each call.

member = Member("Linus", 50)
member.pay_fine(20)
member.pay_fine(20)
member.pay_fine(20)

Stuck? Reveal one hint at a time.

  1. Hint 1

    Each pay_fine call delegates to account.withdraw with the current balance at that point — not the original starting balance.

  2. Hint 2

    The third call needs to be checked against whatever the balance actually is by then, not against 50.

Reveal the trace

Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.

Member("Linus", 50)   -> balance = 50
pay_fine(20)            -> 20 <= 50, allowed -> balance = 30
pay_fine(20)            -> 20 <= 30, allowed -> balance = 10
pay_fine(20)            -> 20 <= 10? No -> refused, balance stays 10

Practice: apply it

This version of Member doesn’t compose Account — it reimplements balance tracking directly instead:

class Member:
    def __init__(self, name, starting_balance):
        self.name = name
        self.balance = starting_balance

    def pay_fine(self, amount):
        if amount > self.balance:
            return False
        self.balance = self.balance - amount
        return True
What's the risk with this version compared to the one that composes Account?
Or reveal the answer without checking

Answer:It duplicates Account's balance logic entirely — any future fix or rule added to Account (like a transaction log) never applies here, since this code never uses Account at all
This Member reimplements exactly what Account already does. Today it behaves the same, but the two implementations can now drift apart — a fix made to Account's withdraw() logic has no effect on this duplicated version.

Modification challenge: rewrite this version of Member to compose an Account instead — store self.account = Account(starting_balance) and have pay_fine delegate to self.account.withdraw(amount).

Summary

  • Composition means an object holds another object as an attribute and delegates to it, rather than reimplementing its behavior.
  • It models a “has-a” relationship directly: a member has an account, rather than being one.
  • Every rule the composed object enforces applies automatically through delegation, without the containing object needing to know how.
  • Copying another class’s logic instead of composing an instance of it duplicates every rule that logic enforces — and lets the two versions drift apart over time.

Key terms