Composition and Relationships Checkpoint
25 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)Fix the mistakes
The slips that cost marks in this module. Spot each one, then check the fix.
Test your design sense on small systems. For each one, decide who holds what, who creates what, and whether a parent class is justified.
Pick the relationship first
Say the sentence out loud. "An Invoice is a Customer" is wrong, "an Invoice has a Customer" is right. Wrong sentences mean wrong code.
class Customer: def __init__(self, name): self.name = name
class Invoice: def __init__(self, customer, amount): self.customer = customer self.amount = amountExample 1 · A system built from parts
class Customer: def __init__(self, name): self.name = name
class Invoice: def __init__(self, customer, amount): self.customer = customer self.amount = amount
def summary(self): return f"{self.customer.name} owes {self.amount}"
class Ledger: def __init__(self): self._invoices = []
def add(self, invoice): self._invoices.append(invoice)
def total(self): return sum(inv.amount for inv in self._invoices)
first = Invoice(Customer("Asha"), 400)ledger = Ledger()ledger.add(first)ledger.add(Invoice(Customer("Ravi"), 250))print(ledger.total())print(first.summary())650 Asha owes 400
What this code does
- Ledger holds invoices, and each invoice holds a customer.
- No class inherits from another, and none needs to.
Invoice keeps a Customer part and never inherits from it.
Hand the collaborators in
A class that builds its own helpers is hard to change and test. Give it what it needs when you create it.
class Checkout: def __init__(self, pricing, sender): self.pricing = pricing self.sender = senderExample 2 · Checkout with injected collaborators
class FlatTax: def apply(self, amount): return amount + 18
class FakeSender: def __init__(self): self.sent = []
def send(self, to, text): self.sent.append(f"{to}: {text}")
class Checkout: def __init__(self, pricing, sender): self.pricing = pricing self.sender = sender
def pay(self, customer, amount): final = self.pricing.apply(amount) self.sender.send(customer, f"paid {final}") return final
fake = FakeSender()print(Checkout(FlatTax(), fake).pay("Meena", 100))print(fake.sent)118 ['Meena: paid 118']
What this code does
- Checkout never knows which pricing rule or sender it got.
- The fake records the message so the test can read it.
Pricing and sender come from outside, so tests can pass fakes.
Mixin, part or function
A stateless extra behaviour for many classes fits a mixin. A feature with its own data, or one that must be swapped, fits a part.
class ToDictMixin: def to_dict(self): return dict(vars(self))Example 3 · A mixin plus a part
class ToDictMixin: def to_dict(self): return dict(vars(self))
class Auditor: def __init__(self): self.log = []
def record(self, text): self.log.append(text)
class Ticket(ToDictMixin): def __init__(self, ticket_id, auditor): self.ticket_id = ticket_id self.auditor = auditor
def close(self): self.auditor.record(f"closed {self.ticket_id}")
auditor = Auditor()t = Ticket(7, auditor)t.close()print(auditor.log)print(sorted(t.to_dict()))['closed 7'] ['auditor', 'ticket_id']
What this code does
- The mixin adds
to_dict; the Auditor part holds the log. - The dictionary has both attributes, ticket_id and auditor.
If you cannot decide, start with a part; it is easier to change later.
Finish up
Mark complete & continue
Next: String Representation: __str__ vs __repr__