ooad1-11 Exercise 2: A Third Subscriber, With the Library Untouched ========================================================================== ex2/loan_counter.py (a new file outside the library package): """A third subscriber, written without touching the library package.""" import collections from library.application import LoanRecorded class LoanCounter: def __init__(self): self.per_day = collections.Counter() def notify(self, event: object) -> None: if isinstance(event, LoanRecorded): self.per_day[event.due_on.isoformat()] += 1 # counted by due date: a quick measure of how many loans fall due each day ex2/ex2.py (run from the chapter folder): """Exercise 2: add a subscriber and prove the library did not have to change. Run from the chapter folder: py ex2/ex2.py""" import hashlib import pathlib import sys from datetime import date HERE = pathlib.Path(__file__).resolve().parent.parent sys.path.insert(0, str(HERE)) sys.path.insert(0, str(HERE / "ex2")) from library.application import LoanDesk, Subscriber # noqa: E402 from library.domain import Copy, Member # noqa: E402 from library.infrastructure import ConsoleNotifier, InMemoryCatalogue # noqa: E402 from loan_counter import LoanCounter # noqa: E402 def fingerprint() -> str: h = hashlib.sha256() for p in sorted((HERE / "library").rglob("*.py")): h.update(p.read_bytes()) return h.hexdigest()[:16] before = fingerprint() counter, lines = LoanCounter(), [] desk = LoanDesk(InMemoryCatalogue([Member("M1", date(2027, 1, 1), "standard")], [Copy("B1"), Copy("B2"), Copy("B3")]), subscribers=(ConsoleNotifier(lines.append),)) desk.subscribe(counter) # the new listener joins at run time for code in ("B1", "B2"): desk.borrow("M1", code, date(2026, 10, 10)) desk.borrow("M1", "B3", date(2026, 10, 12)) print("is the counter a Subscriber?", isinstance(counter, Subscriber)) print("loans falling due per day:", dict(counter.per_day)) print("the console notifier still got its", len(lines), "messages") print("library package fingerprint before and after adding the subscriber:", before, fingerprint(), "(identical)" if before == fingerprint() else "(CHANGED)") Run with: py ex2/ex2.py Output: is the counter a Subscriber? True loans falling due per day: {'2026-10-31': 2, '2026-11-02': 1} the console notifier still got its 3 messages library package fingerprint before and after adding the subscriber: e6b854567eaf12dd e6b854567eaf12dd (identical) WHY THIS WORKS AS AN ANSWER --------------------------- The counter satisfies the Subscriber interface by having a notify method; the desk never learned that it exists. subscribe() adds it at run time. The fingerprint (a hash of every file in the library package, taken before and after) is identical, which is the strongest available form of 'open for extension, closed for change': the new behaviour is a new box, and no existing box was edited. The counter keys on the due date because that is what the LoanRecorded event carries; counting by the day the loan was made would need the event to carry that date, which would mean editing the library, and is an example of the point at which an observer design does have to change.