Advanced Examples
Examples 58-84 assume the full Beginner and Intermediate vocabulary and put it to work at larger scale: refactoring real smells to a pattern under a locking pytest suite (not choosing a pattern up front), naming anti-patterns by their recognizable shape, touring the essential Gang-of-Four catalogue in three gallery sweeps, applying all five SOLID principles and all nine GRASP patterns end-to-end on one order engine, and closing with the finite-state-machine and Harel-statechart rung that goes beyond the object-per-state State pattern. Every example is still a complete, self-contained example.py colocated under learning/code/, verified the same two ways as every earlier tier: python3 example.py prints its own expected output inline via # => comments, and a colocated test_example.py asserts the same behavior under pytest.
Example 58: Refactor Inheritance to Composition
ex-58 · exercises co-33, co-09
A 4-level inheritance hierarchy (Notifier to LoggingNotifier to RetryingLoggingNotifier to ThrottledRetryingLoggingNotifier), each level bolting on one orthogonal capability, is refactored to a single depth-1 class composed with small behavior objects. A locking pytest suite pins both versions' observable behavior across the refactor.
%% Color Palette: Blue #0173B2, Teal #029E73
graph TD
A["Notifier"]:::blue --> B["LoggingNotifier"]:::blue --> C["RetryingLoggingNotifier"]:::blue --> D["ThrottledRetryingLoggingNotifier<br/>(depth 4)"]:::blue
E["ComposedNotifier<br/>(depth 1)"]:::teal -->|composed with| F["LoggingBehavior"]:::teal
E -->|composed with| G["RetryBehavior"]:::teal
E -->|composed with| H["ThrottleBehavior"]:::teal
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
learning/code/ex-58-refactor-inheritance-to-composition/example.py
"""Example 58: Refactor Inheritance to Composition.
co-33 (refactor to pattern): a 4-level inheritance hierarchy, each level bolting on
one orthogonal capability (logging, retry, throttling), is refactored to a single
depth-1 class composed with small behavior objects -- co-09 (grasp-low-coupling):
the composed version needs zero new subclasses to add a capability, only a new
wrapper object. test_example.py locks both versions' observable behavior so the
refactor is verifiably safe.
"""
from __future__ import annotations
from dataclasses import dataclass, field
# ============================================================
# BEFORE: a 4-level inheritance hierarchy, one capability per level
# ============================================================
class Notifier: # => level 1 (base): just sends -- no capabilities yet
def send(self, message: str) -> str: # => returns what was "sent", stubbed as a string for this example
return f"SEND:{message}" # => the delivery itself
class LoggingNotifier(Notifier): # => level 2: bolts on logging via override
def __init__(self) -> None: # => LoggingNotifier's constructor takes no new parameters beyond self
self.log: list[str] = [] # => records every message this notifier has sent
def send(self, message: str) -> str: # => override adds ONE capability: logging
self.log.append(message) # => side effect before delegating up
return super().send(message) # => delegates to level 1's real send
# => level 3 repeats level 2's override-and-delegate shape one layer deeper, adding retry state
class RetryingLoggingNotifier(LoggingNotifier): # => level 3: bolts on retry, on top of logging
def __init__(self, attempts: int) -> None: # => adds one new constructor parameter for this level's capability
super().__init__() # => must still wire up level 2's state
self.attempts = attempts # => how many times to retry
def send(self, message: str) -> str: # => override adds a SECOND capability: retry
last_result = "" # => tracks the most recent attempt's result
for _ in range(self.attempts): # => retry loop wraps the logging+base chain
last_result = super().send(message) # => delegates up two levels each iteration
return last_result # => the final attempt's result
# => level 4 keeps stacking: every new capability means one more override plus one more __init__ chain link
class ThrottledRetryingLoggingNotifier(RetryingLoggingNotifier): # => level 4: bolts on throttling
def __init__(self, attempts: int, max_calls: int) -> None: # => now carries state for capabilities from two different levels
super().__init__(attempts) # => must wire up levels 3 AND 2's __init__ chain
self.max_calls = max_calls # => a THIRD, independent capability: a call budget
self.calls_made = 0 # => how many sends have happened so far
def send(self, message: str) -> str: # => override adds a THIRD capability: throttling
if self.calls_made >= self.max_calls: # => guard: budget exhausted
return "THROTTLED" # => refuse to send once the budget is spent
self.calls_made += 1 # => consume one unit of budget
return super().send(message) # => delegates up the whole three-level chain
# => one send() call now cascades through 4 stacked overrides: throttle -> retry -> logging -> base
# ============================================================
# AFTER: one depth-1 class, capabilities composed as small standalone objects
# ============================================================
@dataclass # => auto-generates __init__ from the single field below
class LoggingBehavior: # => composed equivalent of LoggingNotifier -- one small standalone class
log: list[str] = field(default_factory=list) # => same responsibility, now owned independently
# => this single class replaces all 4 inheritance levels above, using composed collaborators instead of subclassing
class ComposedNotifier: # => depth 1: no subclass at all -- capabilities are composed, not inherited
def __init__(self, logging: LoggingBehavior | None = None) -> None: # => takes an OPTIONAL composed capability instead of a hardcoded subclass chain
self.logging = logging # => an OPTIONAL logging capability, composed in per instance
def raw_send(self, message: str) -> str: # => the equivalent of Notifier.send() in the old hierarchy
if self.logging is not None: # => only log if a LoggingBehavior was composed in
self.logging.log.append(message) # => same log side effect, now opt-in rather than baked into a subclass
return f"SEND:{message}" # => identical delivery stub to the inheritance version
@dataclass # => auto-generates __init__ for the retry capability's single field
class RetryBehavior: # => composed equivalent of the retry capability -- reusable on its own
attempts: int # => how many times to retry
@dataclass # => auto-generates __init__ for the throttle capability's two fields
class ThrottleBehavior: # => composed equivalent of the throttle capability -- reusable on its own
max_calls: int # => the call budget
calls_made: int = 0 # => mutable counter, local to this one small class
# => a free function stands in for a 4th subclass level, since retry/throttle here are simple per-call parameters
def send_with_capabilities( # => a free function stands in for what would have been a 4th subclass level
notifier: ComposedNotifier, # => the base object, already composed with logging
message: str, # => the message to send
retry: RetryBehavior, # => the retry capability, composed in as a parameter
throttle: ThrottleBehavior, # => the throttle capability, composed in as a parameter
) -> str: # => closes the multi-line signature; the function still returns a plain string like the old send()
if throttle.calls_made >= throttle.max_calls: # => throttle checked first, matching the old outermost override
return "THROTTLED" # => matches ThrottledRetryingLoggingNotifier's early return
throttle.calls_made += 1 # => consumes budget exactly once per call, matching the old behavior
last_result = "" # => tracks the final attempt across retries
for _ in range(retry.attempts): # => same retry loop as before, now composed rather than inherited
last_result = notifier.raw_send(message) # => delegates to the composed base (which itself logs)
return last_result # => the final attempt's result
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
old_notifier = ThrottledRetryingLoggingNotifier(attempts=2, max_calls=5) # => 4-level inheritance version
print(old_notifier.send("hello")) # => exercises the full inherited chain
# => Output: SEND:hello
logging_behavior = LoggingBehavior() # => the composed logging unit
new_notifier = ComposedNotifier(logging=logging_behavior) # => depth-1 composed version
result = send_with_capabilities(new_notifier, "hello", RetryBehavior(attempts=2), ThrottleBehavior(max_calls=5)) # => exercises the composed equivalent
print(result) # => identical output to the inheritance version
# => Output: SEND:hello
print(len(ThrottledRetryingLoggingNotifier.__mro__)) # => old hierarchy: 4 custom classes + object
# => Output: 5
print(ComposedNotifier.__mro__ == (ComposedNotifier, object)) # => new version: depth 1, no intermediate bases
# => Output: TrueRun: python3 example.py
Output:
SEND:hello
SEND:hello
5
Truelearning/code/ex-58-refactor-inheritance-to-composition/test_example.py
"""Example 58: pytest verification that behavior is unchanged after the composition refactor."""
from example import (
ComposedNotifier,
LoggingBehavior,
RetryBehavior,
ThrottledRetryingLoggingNotifier,
ThrottleBehavior,
send_with_capabilities,
)
def test_inheritance_and_composition_versions_agree_on_successful_sends() -> None:
old = ThrottledRetryingLoggingNotifier(attempts=2, max_calls=5) # => 4-level inheritance version
old_result = old.send("hello") # => exercises the full inherited chain
logging = LoggingBehavior() # => the composed logging unit
new = ComposedNotifier(logging=logging) # => depth-1 composed version
new_result = send_with_capabilities(new, "hello", RetryBehavior(attempts=2), ThrottleBehavior(max_calls=5))
assert old_result == new_result == "SEND:hello" # => both versions produce identical output
assert old.log == logging.log == ["hello", "hello"] # => both log every retry attempt identically
def test_throttling_behavior_is_identical_once_budget_is_exhausted() -> None:
old = ThrottledRetryingLoggingNotifier(attempts=1, max_calls=1) # => budget of exactly one call
old.send("first") # => consumes the single-call budget
old_second = old.send("second") # => budget exhausted -- should be throttled
logging = LoggingBehavior()
new = ComposedNotifier(logging=logging)
retry = RetryBehavior(attempts=1)
throttle = ThrottleBehavior(max_calls=1)
send_with_capabilities(new, "first", retry, throttle) # => consumes the composed version's budget too
new_second = send_with_capabilities(new, "second", retry, throttle) # => should also be throttled
assert old_second == new_second == "THROTTLED" # => both versions refuse identically once budget is spent
def test_composed_version_has_inheritance_depth_of_exactly_one() -> None:
assert ComposedNotifier.__mro__ == (ComposedNotifier, object) # => depth 1: no intermediate base classes
assert len(ThrottledRetryingLoggingNotifier.__mro__) == 5 # => old version: 4 custom levels + object
# => Run: pytest -q -- Output: 3 passedVerify: pytest -q
Output:
3 passedKey takeaway: An inheritance hierarchy that bolts on one orthogonal capability per level is a composition problem wearing an is-a costume; refactoring each level to a small standalone behavior object collapses depth to 1 while a locking test suite proves nothing observable changed.
Why it matters: Deep hierarchies like this one are the textbook "fragile base class" -- changing level 2's logging logic risks breaking levels 3 and 4, which both silently depend on it through super(). Production codebases that grow capability-per-subclass hierarchies (payment processors, HTTP clients, notification systems) eventually hit a combinatorial wall where every new orthogonal capability doubles the subclass count; composition keeps that growth linear instead of multiplicative.
Example 59: Refactor to Strategy
ex-59 · exercises co-33, co-25
An embedded if/elif chain computing shipping cost by method name is refactored to a strategy family under a locking test suite that stays green through every step. Each shipping method becomes its own small callable, and the dispatcher looks the method up in a table instead of branching on it.
learning/code/ex-59-refactor-to-strategy/example.py
"""Example 59: Refactor to Strategy.
co-33 (refactor to pattern): an embedded if-chain computing shipping cost by method
name is refactored to a strategy family under a locking test suite that stays green
through every step -- co-25 (strategy): each shipping method becomes its own small
callable, and the dispatcher looks the method up instead of branching on it.
"""
from __future__ import annotations
from typing import Callable
# ============================================================
# BEFORE: an if-chain that must be edited for every new shipping method
# ============================================================
def shipping_cost_if_chain(method: str, weight_kg: float) -> float: # => the original, unrefactored dispatcher
if method == "standard": # => branch 1: flat rate per kg
return 2.0 * weight_kg # => $2/kg
elif method == "express": # => branch 2: higher flat rate
return 5.0 * weight_kg # => $5/kg
elif method == "overnight": # => branch 3: highest flat rate plus a surcharge
return 10.0 * weight_kg + 15.0 # => $10/kg plus a $15 fixed surcharge
raise ValueError(f"unknown shipping method: {method}") # => every new method needs a new elif here (OCP violation)
# ============================================================
# AFTER: a strategy family -- new methods are added, not edited into a switch
# ============================================================
ShippingStrategy = Callable[[float], float] # => a strategy is just "weight_kg -> cost", nothing more
def standard_shipping(weight_kg: float) -> float: # => strategy 1, extracted verbatim from the if-chain
return 2.0 * weight_kg # => identical formula to the "standard" branch above
def express_shipping(weight_kg: float) -> float: # => strategy 2, extracted verbatim
return 5.0 * weight_kg # => identical formula to the "express" branch above
def overnight_shipping(weight_kg: float) -> float: # => strategy 3, extracted verbatim
return 10.0 * weight_kg + 15.0 # => identical formula to the "overnight" branch above
# => three strategies extracted so far, each a pure one-line function with the exact old formula
SHIPPING_STRATEGIES: dict[str, ShippingStrategy] = { # => the dispatch table replaces the if-chain entirely
"standard": standard_shipping, # => maps the method name to its strategy function
"express": express_shipping, # => maps a second method name to its strategy function
"overnight": overnight_shipping, # => maps a third method name to its strategy function
} # => closes the dispatch table literal
def shipping_cost_via_strategy(method: str, weight_kg: float) -> float: # => the refactored dispatcher
strategy = SHIPPING_STRATEGIES.get(method) # => look up the strategy by name, no branching required
if strategy is None: # => guard: unknown method still raises, matching the old behavior
raise ValueError(f"unknown shipping method: {method}") # => identical error message to the if-chain version
return strategy(weight_kg) # => delegate to whichever strategy was found
# => this is the OCP payoff: shipping_cost_via_strategy above needs zero edits to support this new method
def bulk_shipping(weight_kg: float) -> float: # => a FOURTH method, added with zero edits to the dispatcher above
return 1.0 * weight_kg - 3.0 # => cheapest rate, bulk discount baked in
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
print(shipping_cost_if_chain("express", 4.0)) # => old dispatcher, exercised for comparison
# => Output: 20.0
print(shipping_cost_via_strategy("express", 4.0)) # => new dispatcher, identical result
# => Output: 20.0
SHIPPING_STRATEGIES["bulk"] = bulk_shipping # => adding "bulk" required zero edits to shipping_cost_via_strategy
print(shipping_cost_via_strategy("bulk", 10.0)) # => the newly registered strategy works immediately
# => Output: 7.0Run: python3 example.py
Output:
20.0
20.0
7.0learning/code/ex-59-refactor-to-strategy/test_example.py
"""Example 59: pytest verification that the strategy refactor stays green throughout."""
import pytest
from example import (
SHIPPING_STRATEGIES,
bulk_shipping,
shipping_cost_if_chain,
shipping_cost_via_strategy,
)
@pytest.mark.parametrize(
"method,weight,expected",
[("standard", 3.0, 6.0), ("express", 4.0, 20.0), ("overnight", 2.0, 35.0)],
) # => same three cases run through both dispatchers, proving the refactor changed nothing observable
def test_old_and_new_dispatchers_agree_on_every_known_method(method: str, weight: float, expected: float) -> None:
assert shipping_cost_if_chain(method, weight) == expected # => old if-chain dispatcher
assert shipping_cost_via_strategy(method, weight) == expected # => new strategy-table dispatcher
def test_both_dispatchers_reject_an_unknown_method_identically() -> None:
with pytest.raises(ValueError, match="unknown shipping method"): # => old dispatcher's error
shipping_cost_if_chain("teleport", 1.0)
with pytest.raises(ValueError, match="unknown shipping method"): # => new dispatcher's error, same message
shipping_cost_via_strategy("teleport", 1.0)
def test_adding_a_new_strategy_requires_zero_edits_to_the_dispatcher() -> None:
SHIPPING_STRATEGIES["bulk"] = bulk_shipping # => registers a fourth strategy at test time
assert shipping_cost_via_strategy("bulk", 10.0) == 7.0 # => works immediately, no dispatcher code changed
del SHIPPING_STRATEGIES["bulk"] # => cleans up so this test does not leak state into other tests
# => Run: pytest -q -- Output: 5 passedVerify: pytest -q
Output:
5 passedKey takeaway: A refactor to strategy is a mechanical, safe move under tests: extract each branch's body verbatim into its own function, build a name-to-function table, and replace the branching with a lookup -- the old and new dispatchers can be run side by side against the same test cases to prove nothing changed.
Why it matters: Long-lived if/elif dispatchers are a magnet for growth -- pricing engines, payment-method routers, and export-format selectors all tend to accumulate branches for years until the function is hundreds of lines long and risky to touch. A strategy table caps that growth: a new case is a new function plus one dictionary entry, reviewable and testable in isolation from every case that came before it.
Example 60: Refactor to State
ex-60 · exercises co-33, co-29
A boolean-flag implementation of order lifecycle (is_paid, is_shipped, is_cancelled, independently settable) allows illegal combinations. Refactoring to the State pattern makes each lifecycle stage its own class, so the current-state object is the only thing deciding which transitions are legal.
%% Color Palette: Blue #0173B2, Teal #029E73, Orange #DE8F05
stateDiagram-v2
[*] --> Created
Created --> Paid: pay()
Paid --> Shipped: ship()
Created --> Created: ship() rejected
Paid --> Paid: pay() rejected
learning/code/ex-60-refactor-to-state/example.py
"""Example 60: Refactor to State.
co-33 (refactor to pattern): a boolean-flag implementation of order lifecycle
(is_paid, is_shipped, is_cancelled -- independently settable) is refactored to the
State pattern -- co-29 (state): each lifecycle stage becomes its own class, and the
current-state object is the only thing that decides which transitions are legal,
making invalid flag combinations unrepresentable.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
# ============================================================
# BEFORE: boolean-flag soup -- nothing prevents illegal combinations
# ============================================================
class FlagOrder: # => the original, unrefactored order
def __init__(self) -> None: # => sets up all three independent boolean flags below
self.is_paid = False # => three independent booleans...
self.is_shipped = False # => ...that can be set in ANY combination...
self.is_cancelled = False # => ...including ones that make no business sense
def mark_shipped(self) -> None: # => nothing here checks is_paid first
self.is_shipped = True # => BUG: this can fire even when is_paid is still False
# => nothing in FlagOrder's design stops mark_shipped() from running before is_paid is ever set
# ============================================================
# AFTER: the State pattern -- illegal transitions are structurally impossible
# ============================================================
class OrderState(ABC): # => the shared interface every concrete state implements
@abstractmethod # => forces every concrete state to define its own pay() behavior
def pay(self, order: "StateOrder") -> None: # => only states where paying is legal override this meaningfully
raise NotImplementedError # => abstract method body, never actually executed
@abstractmethod # => forces every concrete state to define its own ship() behavior
def ship(self, order: "StateOrder") -> None: # => only the Paid state allows shipping
raise NotImplementedError # => abstract method body, never actually executed
def name(self) -> str: # => a readable label for the current state, used in tests and printing
return type(self).__name__ # => e.g. "Created", "Paid", "Shipped"
class Created(OrderState): # => the starting state: nothing has happened yet
def pay(self, order: "StateOrder") -> None: # => the ONLY legal transition out of Created
order.state = Paid() # => moves the order into the Paid state
def ship(self, order: "StateOrder") -> None: # => shipping from Created is illegal
raise ValueError("cannot ship an order that has not been paid") # => structurally rejected, not just unwise
# => Paid is reachable only via Created.pay() above -- there is no way to construct an order that skips it
class Paid(OrderState): # => after payment, before shipping
def pay(self, order: "StateOrder") -> None: # => paying twice is illegal
raise ValueError("order is already paid") # => Paid has no legal pay() transition
def ship(self, order: "StateOrder") -> None: # => the ONLY legal transition out of Paid
order.state = Shipped() # => moves the order into the Shipped state
# => Shipped is the terminal state: both its methods only ever raise, matching a real "closed" order
class Shipped(OrderState): # => the terminal happy-path state
def pay(self, order: "StateOrder") -> None: # => paying a shipped order is illegal
raise ValueError("order is already shipped") # => Shipped has no legal pay() transition
def ship(self, order: "StateOrder") -> None: # => shipping twice is illegal
raise ValueError("order is already shipped") # => Shipped has no legal ship() transition
# => StateOrder itself contains ZERO business rules -- every legality check lives inside the state classes above
class StateOrder: # => the refactored order -- delegates every transition to its current state object
def __init__(self) -> None: # => starts every new order in a single, well-defined state
self.state: OrderState = Created() # => starts in the Created state, same as FlagOrder's initial flags
def pay(self) -> None: # => delegates to whichever state object is current
self.state.pay(self) # => the STATE decides whether this is legal, not an if-chain here
def ship(self) -> None: # => delegates to whichever state object is current
self.state.ship(self) # => "shipped without paid" cannot happen: Created.ship() always raises
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
flag_order = FlagOrder() # => the old version
flag_order.mark_shipped() # => BUG reproduced: shipped is True even though paid is still False
print(flag_order.is_shipped, flag_order.is_paid) # => demonstrates the illegal combination the flags allow
# => Output: True False
state_order = StateOrder() # => the refactored version
try: # => wraps the illegal transition attempt so its ValueError can be shown
state_order.ship() # => attempting the same illegal move
except ValueError as exc: # => catches the rejection raised by Created.ship()
print(exc) # => the State pattern rejects it outright
# => Output: cannot ship an order that has not been paid
state_order.pay() # => legal: Created -> Paid
state_order.ship() # => legal: Paid -> Shipped
print(state_order.state.name()) # => confirms the final state
# => Output: ShippedRun: python3 example.py
Output:
True False
cannot ship an order that has not been paid
Shippedlearning/code/ex-60-refactor-to-state/test_example.py
"""Example 60: pytest verification that state-object refactor makes illegal combos impossible."""
import pytest
from example import Created, FlagOrder, StateOrder
def test_flag_order_allows_the_illegal_shipped_without_paid_combination() -> None:
order = FlagOrder() # => reproduces the bug the refactor fixes
order.mark_shipped() # => nothing stops this even though is_paid is still False
assert order.is_shipped is True and order.is_paid is False # => the illegal combination exists
def test_state_order_rejects_shipping_before_payment() -> None:
order = StateOrder() # => starts in Created
with pytest.raises(ValueError, match="not been paid"): # => the illegal move is structurally rejected
order.ship()
assert isinstance(order.state, Created) # => the order never left Created
def test_state_order_allows_the_legal_created_to_paid_to_shipped_path() -> None:
order = StateOrder()
order.pay() # => Created -> Paid
order.ship() # => Paid -> Shipped
assert order.state.name() == "Shipped" # => reached the terminal state via only legal transitions
def test_state_order_rejects_paying_twice() -> None:
order = StateOrder()
order.pay() # => first pay is legal
with pytest.raises(ValueError, match="already paid"): # => second pay is illegal
order.pay()
# => Run: pytest -q -- Output: 4 passedVerify: pytest -q
Output:
4 passedKey takeaway: Boolean flags can express any combination, legal or not; refactoring lifecycle state into explicit state objects makes illegal combinations a compile-time-absent method call rather than a runtime bug waiting to be triggered.
Why it matters: "Shipped but not paid" bugs are a recurring class of production incident in e-commerce and billing systems -- exactly the kind of invalid state boolean flags permit silently. The State pattern converts that class of bug into a ValueError raised at the exact call site that attempted the illegal move, catchable by a test long before it reaches production.
Example 61: Refactor God Object
ex-61 · exercises co-33, co-01, co-06
A god object mixing user validation, order pricing, and receipt formatting is decomposed by applying SRP and information-expert together -- each responsibility moves to the class that already holds the data it needs, and a locking test suite proves the decomposition preserves behavior.
learning/code/ex-61-refactor-god-object/example.py
"""Example 61: Refactor God Object.
co-33 (refactor to pattern): a god object mixing user validation, order pricing, and
receipt formatting is decomposed by applying co-01 (SRP) and co-06 (grasp-information-
expert) -- each responsibility moves to the class that already holds the data it
needs, and a locking test suite proves the decomposition preserves behavior.
"""
from __future__ import annotations
from dataclasses import dataclass, field
# ============================================================
# BEFORE: one class doing validation, pricing, AND formatting
# ============================================================
class GodShop: # => three unrelated responsibilities crammed into one class
def __init__(self) -> None: # => sets up the shared price dictionary used by all three responsibilities
self.items: dict[str, float] = {} # => item name -> unit price, used by pricing
def add_item(self, name: str, price: float) -> None: # => pricing-related setup
self.items[name] = price # => stores the price for later total() calls
def is_valid_email(self, email: str) -> bool: # => VALIDATION concern, unrelated to pricing/formatting
return "@" in email and "." in email.split("@")[-1] # => a minimal, deliberately simple check
def total(self, item_names: list[str]) -> float: # => PRICING concern
return sum(self.items[name] for name in item_names) # => sums the requested items' prices
def format_receipt(self, item_names: list[str]) -> str: # => FORMATTING concern
lines = [f"{name}: ${self.items[name]:.2f}" for name in item_names] # => one line per item
lines.append(f"TOTAL: ${self.total(item_names):.2f}") # => reuses total(), coupling formatting to pricing
return "\n".join(lines) # => the full receipt as one string
# ============================================================
# AFTER: three cohesive classes, one responsibility each
# ============================================================
class EmailValidator: # => SRP: validation, and only validation
def is_valid(self, email: str) -> bool: # => identical logic to GodShop.is_valid_email, now isolated
return "@" in email and "." in email.split("@")[-1] # => same check, moved to its own home
@dataclass # => auto-generates __init__ from the prices field below
class Catalog: # => grasp-information-expert: Catalog owns the prices, so it computes totals
prices: dict[str, float] = field(default_factory=dict) # => item name -> unit price
def add_item(self, name: str, price: float) -> None: # => pricing-related setup, unchanged behavior
self.prices[name] = price # => same assignment as GodShop.add_item, now scoped to pricing only
def total(self, item_names: list[str]) -> float: # => information-expert: Catalog holds prices, so it sums them
return sum(self.prices[name] for name in item_names) # => identical formula to GodShop.total
class ReceiptFormatter: # => SRP: formatting, and only formatting -- depends on Catalog, not the other way around
def __init__(self, catalog: Catalog) -> None: # => depends on Catalog rather than owning prices itself
self.catalog = catalog # => the one collaborator this class needs
def format(self, item_names: list[str]) -> str: # => identical output to GodShop.format_receipt
lines = [f"{name}: ${self.catalog.prices[name]:.2f}" for name in item_names] # => same per-line format
lines.append(f"TOTAL: ${self.catalog.total(item_names):.2f}") # => delegates pricing to Catalog, not itself
return "\n".join(lines) # => identical join to the god-object version
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
god = GodShop() # => the original god object
god.add_item("Book", 12.5) # => registers a price on the god object
god.add_item("Pen", 1.5) # => registers a second price on the god object
print(god.format_receipt(["Book", "Pen"])) # => exercises all three responsibilities through one class
# => Output: Book: $12.50
# => Pen: $1.50
# => TOTAL: $14.00
catalog = Catalog() # => the decomposed version
catalog.add_item("Book", 12.5) # => registers the same price on the decomposed Catalog
catalog.add_item("Pen", 1.5) # => registers the same second price on the decomposed Catalog
formatter = ReceiptFormatter(catalog) # => formatter depends on catalog, not the reverse
print(formatter.format(["Book", "Pen"])) # => identical output, now via three cohesive classes
# => Output: Book: $12.50
# => Pen: $1.50
# => TOTAL: $14.00Run: python3 example.py
Output:
Book: $12.50
Pen: $1.50
TOTAL: $14.00
Book: $12.50
Pen: $1.50
TOTAL: $14.00learning/code/ex-61-refactor-god-object/test_example.py
"""Example 61: pytest verification that the god-object decomposition preserves behavior."""
from example import Catalog, EmailValidator, GodShop, ReceiptFormatter
def test_god_object_and_decomposed_version_produce_identical_receipts() -> None:
god = GodShop()
god.add_item("Book", 12.5)
god.add_item("Pen", 1.5)
catalog = Catalog()
catalog.add_item("Book", 12.5)
catalog.add_item("Pen", 1.5)
formatter = ReceiptFormatter(catalog)
assert god.format_receipt(["Book", "Pen"]) == formatter.format(["Book", "Pen"]) # => byte-identical output
def test_email_validation_is_identical_after_extraction() -> None:
god = GodShop()
validator = EmailValidator() # => the extracted, standalone responsibility
for email in ["a@b.com", "not-an-email", "x@y"]: # => same three cases through both implementations
assert god.is_valid_email(email) == validator.is_valid(email) # => behavior is unchanged after extraction
def test_each_decomposed_class_has_exactly_one_responsibility() -> None:
validator = EmailValidator()
assert not hasattr(validator, "total") # => EmailValidator knows nothing about pricing
catalog = Catalog()
assert not hasattr(catalog, "format") # => Catalog knows nothing about formatting
assert not hasattr(catalog, "is_valid") # => Catalog knows nothing about validation
# => Run: pytest -q -- Output: 3 passedVerify: pytest -q
Output:
3 passedKey takeaway: Decomposing a god object is a two-question exercise per method -- "what is this method's one reason to change" (SRP) and "which class already holds the data this needs" (information-expert) -- applied together they produce a natural split, not an arbitrary one.
Why it matters: God objects accumulate in real codebases when "just one more method" repeatedly seems like the path of least resistance -- a UserManager or OrderService that grows to hundreds of methods across unrelated concerns. Decomposing under a locking test suite is what makes the refactor safe to ship incrementally, rather than a risky big-bang rewrite.
Example 62: Anti-Pattern -- God Object
ex-62 · exercises co-34
A god object is diagnosed by a simple, objective metric: the number of distinct, unrelated responsibilities (auth, email, reporting) a single class carries. The fix is sketched -- named services, one per responsibility -- as a decision record, not fully executed here (Example 61 executes the equivalent fix for a smaller case).
learning/code/ex-62-anti-pattern-god-object/example.py
"""Example 62: Anti-Pattern -- God Object.
co-34 (anti-pattern recognition): a god object is diagnosed by a simple, objective
metric -- the number of DISTINCT responsibilities (unrelated method groups) a single
class carries -- and a fix is sketched (not fully executed here; Example 61 executes
the equivalent fix for a smaller case) as "split by reason-to-change."
"""
from __future__ import annotations
# => this class deliberately mixes concerns to demonstrate the smell -- Example 61 shows the same smell fixed
class ApplicationManager: # => a god object: user auth, email sending, AND report generation, all in one class
def __init__(self) -> None: # => sets up state for TWO of the three responsibilities this god object carries
self.users: dict[str, str] = {} # => username -> password, for AUTH
self.sent_emails: list[str] = [] # => log of sent emails, for EMAIL
# => below: two methods share the SAME state (self.users) -- a genuine, cohesive AUTH pair
def register_user(self, username: str, password: str) -> None: # => AUTH responsibility
self.users[username] = password # => mutates the AUTH state dict directly
def authenticate(self, username: str, password: str) -> bool: # => AUTH responsibility
return self.users.get(username) == password # => reads the same AUTH state dict to check credentials
# => this method touches a DIFFERENT piece of state (self.sent_emails) than the two AUTH methods above
def send_email(self, to: str, subject: str) -> None: # => EMAIL responsibility, unrelated to auth
self.sent_emails.append(f"{to}:{subject}") # => mutates the EMAIL state list, a completely different concern
# => this method touches NEITHER self.users NOR self.sent_emails -- it is a pure function bolted onto the class
def generate_report(self, title: str, rows: list[str]) -> str: # => REPORTING responsibility, unrelated to both
return f"# {title}\n" + "\n".join(rows)
# => this function is the "objective metric" the docstring promises: it counts concerns, not lines of code
def count_distinct_responsibilities(cls: type) -> int: # => a simple, mechanical god-object detector
method_prefixes = { # => groups methods by a naming-convention proxy for "responsibility"
"register_user": "auth", # => maps each method name to the concern it actually serves
"authenticate": "auth", # => a second AUTH-concern method
"send_email": "email", # => the EMAIL-concern method
"generate_report": "reporting", # => the REPORTING-concern method
} # => closes the method-to-concern mapping dict
methods_on_class = [name for name in method_prefixes if hasattr(cls, name)] # => which of these methods exist
concerns = {method_prefixes[name] for name in methods_on_class} # => the DISTINCT concerns those methods serve
return len(concerns) # => 1 concern = cohesive class; 2+ = candidate god object
# => intentionally a sketch, not a real refactor: Example 61 shows the equivalent split fully executed and tested
def sketch_fix() -> list[str]: # => the fix is SKETCHED here (named responsibilities), not fully executed
return [ # => opens the list of sketched, human-readable split targets
"AuthService: register_user(), authenticate() -- owns self.users", # => split 1
"EmailService: send_email() -- owns self.sent_emails", # => split 2
"ReportService: generate_report() -- owns nothing, pure function of its arguments", # => split 3
] # => closes the sketched-fix list
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
count = count_distinct_responsibilities(ApplicationManager) # => the diagnostic metric
print(count) # => 3 distinct concerns in ONE class -- the god-object smell, named
# => Output: 3
# => count_distinct_responsibilities is deliberately crude (a naming-convention lookup), but names the smell
for line in sketch_fix(): # => print the sketched split, one responsibility per line
print(line) # => prints one sketched responsibility split per line
# => Output: AuthService: register_user(), authenticate() -- owns self.users
# => Output: EmailService: send_email() -- owns self.sent_emails
# => Output: ReportService: generate_report() -- owns nothing, pure function of its argumentsRun: python3 example.py
Output:
3
AuthService: register_user(), authenticate() -- owns self.users
EmailService: send_email() -- owns self.sent_emails
ReportService: generate_report() -- owns nothing, pure function of its argumentslearning/code/ex-62-anti-pattern-god-object/test_example.py
"""Example 62: pytest verification that the god-object smell is named and a fix sketched."""
from example import ApplicationManager, count_distinct_responsibilities, sketch_fix
def test_god_object_is_named_by_counting_distinct_responsibilities() -> None:
assert count_distinct_responsibilities(ApplicationManager) == 3 # => auth + email + reporting, three concerns
def test_fix_is_sketched_with_one_line_per_extracted_responsibility() -> None:
fix = sketch_fix()
assert len(fix) == 3 # => one sketched service per responsibility identified above
assert any("AuthService" in line for line in fix) # => the auth split is named
assert any("EmailService" in line for line in fix) # => the email split is named
assert any("ReportService" in line for line in fix) # => the reporting split is named
# => Run: pytest -q -- Output: 2 passedVerify: pytest -q
Output:
2 passedKey takeaway: A god object is named, not felt -- count how many unrelated concerns a class's methods actually serve; more than one concern is a concrete, checkable signal, not a vague impression that "this class is too big."
Why it matters: God objects (Utils, Manager, Service classes that absorb every new feature) are one of the most common real-world smells because adding "just one more method" to an existing class is always the path of least resistance in the moment. Naming the smell with a concrete metric turns a code-review debate ("is this class too big?") into an objective check a team can agree on.
Example 63: Anti-Pattern -- Anemic Domain Model
ex-63 · exercises co-34, co-06
An anemic AnemicAccount is a data class with all behavior pulled into a separate AccountService, so nothing stops a caller from bypassing the invariant via direct field mutation. Moving withdraw() onto the entity itself (information-expert) makes the invariant unbypassable.
learning/code/ex-63-anti-pattern-anemic-domain/example.py
"""Example 63: Anti-Pattern -- Anemic Domain Model.
co-34 (anti-pattern recognition): an anemic model is a data class with all behavior
pulled into a separate service, so the "object" in object-oriented is data-only --
co-06 (grasp-information-expert) names the fix: move behavior onto the entity that
already holds the data the behavior needs.
"""
from __future__ import annotations
from dataclasses import dataclass
# ============================================================
# BEFORE: an anemic Account (data only) plus a service that does everything
# ============================================================
@dataclass # => auto-generates __init__ from the balance field below
class AnemicAccount: # => pure data -- no behavior at all, just fields
balance: float # => the only thing this "object" holds
# => contrast: nothing here stops other code from mutating account.balance directly, bypassing withdraw()
class AccountService: # => ALL the behavior lives here instead, disconnected from the data it operates on
def withdraw(self, account: AnemicAccount, amount: float) -> None: # => reaches INTO the account to mutate it
if amount > account.balance: # => the invariant ("cannot overdraw") lives in the service, not the entity
raise ValueError("insufficient funds") # => nothing stops OTHER code from bypassing this check
account.balance -= amount # => direct field mutation, invariant enforced only if you remembered to call this
# => this mutation is only safe if every caller remembers to call withdraw() instead of touching balance
# ============================================================
# AFTER: the entity owns its own behavior -- information-expert applied
# ============================================================
# => RichAccount fixes the anemia: balance and the rule that guards it now live in the same place
class RichAccount: # => the entity now owns the data AND the behavior that guards it
def __init__(self, balance: float) -> None: # => takes the initial balance directly, same shape as AnemicAccount
self._balance = balance # => the same data, but now private -- no direct external mutation
@property # => exposes balance for reading without exposing a way to write it directly
def balance(self) -> float: # => read-only view of the balance
return self._balance # => the only way to read balance from outside the class
def withdraw(self, amount: float) -> None: # => information-expert: RichAccount holds balance, so IT withdraws
if amount > self._balance: # => the SAME invariant, but now impossible to bypass via direct field access
raise ValueError("insufficient funds") # => identical error to the anemic version
self._balance -= amount # => the only way balance can change is through this guarded method
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
anemic = AnemicAccount(balance=100.0) # => the old version
service = AccountService() # => the disconnected service the old design requires
service.withdraw(anemic, 30.0) # => behavior lives OUTSIDE the object it operates on
print(anemic.balance) # => still correct here, but nothing prevents anemic.balance = 999 elsewhere
# => Output: 70.0
# => same starting balance as the anemic example above, for a fair side-by-side comparison
rich = RichAccount(balance=100.0) # => the refactored version
rich.withdraw(30.0) # => behavior lives ON the object, next to the data it guards
print(rich.balance) # => identical result to the anemic version
# => Output: 70.0Run: python3 example.py
Output:
70.0
70.0learning/code/ex-63-anti-pattern-anemic-domain/test_example.py
"""Example 63: pytest verification that behavior moved onto the entity."""
import pytest
from example import AccountService, AnemicAccount, RichAccount
def test_anemic_version_lets_the_invariant_be_bypassed_via_direct_field_access() -> None:
account = AnemicAccount(balance=100.0)
account.balance = -50.0 # => NOTHING stops this: the invariant only lives in AccountService.withdraw
assert account.balance == -50.0 # => the anemic model allows an invalid balance to exist
def test_rich_account_enforces_the_same_invariant_and_cannot_be_bypassed() -> None:
account = RichAccount(balance=100.0)
with pytest.raises(AttributeError): # => balance has no setter -- direct mutation is impossible
account.balance = -50.0 # type: ignore[misc] # => deliberately demonstrates the blocked mutation path
account.withdraw(30.0) # => the only sanctioned way to change balance
assert account.balance == 70.0
def test_both_versions_agree_on_a_legal_withdrawal() -> None:
anemic = AnemicAccount(balance=100.0)
AccountService().withdraw(anemic, 40.0)
rich = RichAccount(balance=100.0)
rich.withdraw(40.0)
assert anemic.balance == rich.balance == 60.0 # => same math, different ownership of the behavior
# => Run: pytest -q -- Output: 3 passedVerify: pytest -q
Output:
3 passedKey takeaway: A class with fields but no behavior is not "simple" -- it is a promise, unenforced, that some other code will always remember to guard its invariants; moving the guarding method onto the entity turns that promise into a language-level guarantee.
Why it matters: Anemic models are common in codebases influenced by early Java-bean or plain-DTO conventions, where "the service layer does the logic." The cost shows up as invariants that quietly erode over years, because every new service that touches the entity has to remember (and correctly reimplement) the same guard rather than calling one method that always enforces it.
Example 64: Anti-Pattern -- Yo-Yo Inheritance
ex-64 · exercises co-34
A four-level pricing hierarchy makes understanding discount() require jumping up through super() calls across all four classes -- the "yo-yo" smell. A mechanical hop-counter names the smell as a number, and flattening to one function collapses it to zero jumps with an identical result.
learning/code/ex-64-anti-pattern-yo-yo/example.py
"""Example 64: Anti-Pattern -- Yo-Yo Inheritance.
co-34 (anti-pattern recognition): yo-yo inheritance is diagnosed when understanding
one method call requires jumping up and down a deep hierarchy, following super()
calls back and forth across many classes. The fix is flattening: collapse the chain
to the minimum depth that still expresses a genuine is-a relationship.
"""
from __future__ import annotations
# ============================================================
# BEFORE: understanding discount() requires jumping through 4 classes
# ============================================================
class BasePricer: # => level 1
def discount(self, price: float) -> float: # => defines the base case
return price # => no discount at this level
class RegionalPricer(BasePricer): # => level 2 -- must jump UP to BasePricer to see the base case
def discount(self, price: float) -> float: # => override signature, unchanged from BasePricer's
base = super().discount(price) # => jump UP to level 1
return base * 0.95 # => 5% regional discount, only visible by reading THIS override
class SeasonalPricer(RegionalPricer): # => level 3 -- must jump UP through level 2 AND level 1
def discount(self, price: float) -> float: # => override signature, unchanged from the level below
base = super().discount(price) # => jump UP to level 2, which itself jumps UP to level 1
return base * 0.90 # => additional 10% seasonal discount
class LoyaltyPricer(SeasonalPricer): # => level 4 -- understanding this needs all THREE super() calls above it
def discount(self, price: float) -> float: # => override signature, unchanged from all three levels below
base = super().discount(price) # => jump UP to level 3, which jumps to level 2, which jumps to level 1
return base * 0.98 # => additional 2% loyalty discount -- the "yo-yo": up, up, up just to see the total
# => this function literalizes "yo-yo" as a number: how many classes must be opened to read one method fully
def count_hops_to_understand(cls: type, method_name: str) -> int: # => a mechanical yo-yo detector
hops = 0 # => counts how many classes in the MRO define this method
for klass in cls.__mro__: # => walk the method resolution order top to bottom
if method_name in klass.__dict__: # => this class DEFINES (not just inherits) the method
hops += 1 # => reading the full behavior requires visiting this class too
return hops # => 4+ hops for one method is the yo-yo smell
# ============================================================
# AFTER: flattened to depth 1 -- discount is one function, fully readable in place
# ============================================================
def flattened_discount(price: float) -> float: # => the SAME final formula, with zero jumps required to read it
return price * 0.95 * 0.90 * 0.98 # => regional * seasonal * loyalty, all visible on one line
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
print(count_hops_to_understand(LoyaltyPricer, "discount")) # => 4 classes must be read to understand ONE method
# => Output: 4
loyalty = LoyaltyPricer() # => instantiating the deepest, most yo-yo-prone class
print(round(loyalty.discount(100.0), 4)) # => the yo-yo version's result
# => Output: 83.79
print(round(flattened_discount(100.0), 4)) # => the flattened version's result -- identical, zero jumps
# => Output: 83.79Run: python3 example.py
Output:
4
83.79
83.79learning/code/ex-64-anti-pattern-yo-yo/test_example.py
"""Example 64: pytest verification that yo-yo inheritance is diagnosed and flattened."""
from example import LoyaltyPricer, count_hops_to_understand, flattened_discount
def test_yo_yo_hierarchy_requires_four_hops_to_understand_one_method() -> None:
assert count_hops_to_understand(LoyaltyPricer, "discount") == 4 # => the smell, named as a number
def test_flattened_version_produces_the_identical_result() -> None:
loyalty = LoyaltyPricer()
original = round(loyalty.discount(100.0), 6) # => the yo-yo version's result
flattened = round(flattened_discount(100.0), 6) # => the flattened version's result
assert original == flattened # => flattening changed nothing observable
def test_flattened_version_is_a_single_plain_function_with_no_hierarchy_at_all() -> None:
assert flattened_discount.__module__ == "example" # => defined once, at module level -- no MRO to walk at all
assert not hasattr(flattened_discount, "__mro__") # => a plain function has no inheritance chain to jump through
# => Run: pytest -q -- Output: 3 passedVerify: pytest -q
Output:
3 passedKey takeaway: Yo-yo inheritance is measurable -- count how many classes in a hierarchy define (not merely inherit) the method you're trying to understand; a number that keeps growing as the hierarchy deepens is the smell, independent of anyone's subjective sense that "this is confusing."
Why it matters: Deep hierarchies built one incremental override at a time (each one individually reasonable) compound into a debugging tax: fixing a bug in LoyaltyPricer.discount() requires reading three other classes to know what "the base behavior" even is before you can tell whether your fix is correct. Flattening trades a taxonomy for a formula, trading inheritance's illusion of reuse for actual readability.
Example 65: Anti-Pattern -- Singleton Abuse
ex-65 · exercises co-34, co-19
A RequestCounter singleton used as a shared mutable counter demonstrates hidden global coupling directly: two unrelated-looking functions silently share state through it, and a test that forgets to reset the singleton inherits state left behind by whichever test ran before it.
learning/code/ex-65-anti-pattern-singleton-abuse/example.py
"""Example 65: Anti-Pattern -- Singleton Abuse.
co-34 (anti-pattern recognition) + co-19 (singleton and its costs): a singleton used
as a shared mutable counter is hidden global coupling -- any test that runs before
another can silently leak state into it, because nothing in either test's signature
reveals the dependency exists.
"""
from __future__ import annotations # => defers type-hint evaluation, letting RequestCounter reference itself
# => the singleton pattern itself: __new__ ensures only ONE instance is ever created, globally
class RequestCounter: # => a classic singleton: one shared instance, reachable globally
# => the Optional sentinel pattern: None means "not constructed yet", the class itself means "constructed"
_instance: "RequestCounter | None" = None # => the one shared instance, or None before first use
def __new__(cls) -> "RequestCounter": # => __new__ is overridden so every RequestCounter() returns the SAME object
if cls._instance is None: # => first call: create the one instance
cls._instance = super().__new__(cls) # => allocate it exactly once
# => count is set dynamically here, never declared in the class body -- static checkers cannot see it
cls._instance.count = 0 # type: ignore[attr-defined] # => initialize state on the shared instance
return cls._instance # => every subsequent call returns this SAME object, not a new one
# => every call to increment(), from ANYWHERE in the program, mutates this one shared counter
def increment(self) -> int: # => mutates the ONE shared instance's state
# => the `# type: ignore[attr-defined]` markers exist because count is never declared as a class field
self.count += 1 # type: ignore[attr-defined] # => hidden global mutation -- no caller passed this in
return self.count # type: ignore[attr-defined]
# => neither handle_request_a nor handle_request_b takes a counter parameter -- the coupling is invisible
def handle_request_a() -> int: # => neither function's SIGNATURE reveals it depends on RequestCounter
counter = RequestCounter() # => silently reaches for the global singleton
return counter.increment() # => mutates shared state as a side effect
# => this function looks completely independent of handle_request_a, yet secretly shares its state
def handle_request_b() -> int: # => a completely unrelated-looking function...
counter = RequestCounter() # => ...that happens to share state with handle_request_a via the singleton
return counter.increment() # => whichever function runs first determines the other's starting count
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
# NOTE: resetting _instance here is itself evidence of the cost -- production code has no clean reset hook
# => contrast with dependency injection: a passed-in counter would need no such manual reset ritual
RequestCounter._instance = None # => manual reset required before every isolated demonstration
# => without the manual reset on the line above, this count would carry over from any earlier run
print(handle_request_a()) # => first call anywhere: count becomes 1
# => Output: 1
print(handle_request_b()) # => a DIFFERENT function, yet it sees count=2 -- hidden coupling via the singleton
# => Output: 2
# => the defining trait of a singleton: two separate construction calls, one identity
print(RequestCounter() is RequestCounter()) # => confirms both calls returned the exact same object
# => Output: TrueRun: python3 example.py
Output:
1
2
Truelearning/code/ex-65-anti-pattern-singleton-abuse/test_example.py
"""Example 65: pytest verification that singleton state leaks silently between tests."""
from example import RequestCounter, handle_request_a, handle_request_b
def test_state_leaks_across_two_unrelated_looking_functions() -> None:
RequestCounter._instance = None # => manual reset required BECAUSE the singleton has no other reset mechanism
a_result = handle_request_a() # => no parameter reveals this depends on shared state
b_result = handle_request_b() # => a DIFFERENT function, yet its result depends on what ran before it
assert a_result == 1 # => first increment anywhere
assert b_result == 2 # => the "hidden coupling" pain: b's result depends on a having run first
def test_forgetting_the_manual_reset_leaks_state_into_the_next_test() -> None:
# => this test intentionally does NOT reset _instance first, demonstrating the isolation cost directly
leaked_start = RequestCounter().count # type: ignore[attr-defined] # => count is whatever the PREVIOUS test left
assert leaked_start > 0 # => proof: state from test_state_leaks_across_two_unrelated_looking_functions survived
# => Run: pytest -q -- Output: 2 passedVerify: pytest -q
Output:
2 passedKey takeaway: A singleton's danger is not the "one instance" guarantee itself -- it is that the dependency on shared state is invisible in every caller's signature, so isolation requires a manual reset that is easy to forget, as the second test demonstrates by deliberately forgetting it.
Why it matters: Singleton-backed caches, loggers, and configuration objects are a recurring source of flaky test suites in real projects -- a test passing in isolation and failing only when run after another test is the single most common symptom of exactly this coupling. Example 31 shows the fix: inject the shared dependency explicitly instead of reaching for a global.
Example 66: Anti-Pattern -- Premature Abstraction
ex-66 · exercises co-34, co-02
A TaxStrategy interface built for "future" tax rules that never materialized is speculative generality -- exactly one implementation has ever existed. Removing the abstraction and calling the one implementation directly produces the identical result with strictly less ceremony.
learning/code/ex-66-anti-pattern-premature-abstraction/example.py
"""Example 66: Anti-Pattern -- Premature Abstraction.
co-34 (anti-pattern recognition) + co-02 (OCP, its counterpart): a Strategy family
built for a SINGLE implementation ("just in case" a second one shows up) is
speculative generality -- extra indirection paid for before any real variation
exists. The fix is removing the abstraction and calling the one implementation
directly; OCP earns its indirection only once a second variant is real (Example 3).
"""
from __future__ import annotations # => defers type-hint evaluation, not required here but kept for consistency
from abc import ABC, abstractmethod # => the tools used to build the abstraction this example argues against
# ============================================================
# BEFORE: a strategy interface for exactly ONE implementation
# ============================================================
class TaxStrategy(ABC): # => an interface built for "future" tax strategies that do not exist yet
@abstractmethod # => forces every future strategy to implement calculate() -- but only one ever will
def calculate(self, amount: float) -> float: # => the one abstract method every strategy would implement
# => this body is dead code: @abstractmethod already forbids instantiating TaxStrategy directly
raise NotImplementedError # => abstract method body, never actually executed
# => this is the ONLY class that will ever subclass TaxStrategy in this codebase's actual history
class FlatRateTaxStrategy(TaxStrategy): # => the ONLY implementation that has ever existed
def calculate(self, amount: float) -> float: # => the ONLY override that has ever existed or is planned
return amount * 0.08 # => flat 8% tax -- there is no second rule to switch between
# => the strategy parameter exists to support variation that has never materialized
def total_with_tax_premature(amount: float, strategy: TaxStrategy) -> float: # => a parameter for a choice nobody makes
return amount + strategy.calculate(amount) # => every caller passes the SAME FlatRateTaxStrategy() instance
# ============================================================
# AFTER: the abstraction is removed -- one function, one formula
# ============================================================
# => AFTER: same behavior, but the interface, the concrete class, and the parameter are all simply gone
def total_with_tax_simple(amount: float) -> float: # => no interface, no strategy parameter, no indirection
return amount + amount * 0.08 # => the SAME formula, with zero ceremony around it
# => a crude proxy metric, but it makes the added ceremony countable rather than just a feeling
def count_lines_of_indirection(strategy_based: bool) -> int: # => a simple proxy for "how much ceremony"
if strategy_based: # => the premature-abstraction version needs: ABC + abstractmethod + concrete class + param
return 4 # => TaxStrategy class, calculate() signature, FlatRateTaxStrategy class, strategy parameter
return 0 # => the simplified version needs none of that
# => the count itself never varies at runtime -- it exists only to make "more ceremony" a checkable fact
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
# => both branches below compute the exact same tax on the exact same amount -- only the ceremony differs
premature = total_with_tax_premature(100.0, FlatRateTaxStrategy()) # => must construct a strategy just to call this
print(premature) # => the abstraction adds ceremony without adding any actual flexibility used anywhere
# => Output: 108.0
simple = total_with_tax_simple(100.0) # => identical result, zero ceremony
print(simple) # => same number, arrived at directly
# => Output: 108.0
print(count_lines_of_indirection(strategy_based=True) > count_lines_of_indirection(strategy_based=False)) # => confirms the abstraction costs strictly more ceremony
# => Output: TrueRun: python3 example.py
Output:
108.0
108.0
Truelearning/code/ex-66-anti-pattern-premature-abstraction/test_example.py
"""Example 66: pytest verification that removing a premature abstraction simplifies the code."""
from example import (
FlatRateTaxStrategy,
count_lines_of_indirection,
total_with_tax_premature,
total_with_tax_simple,
)
def test_both_versions_produce_the_identical_result() -> None:
premature = total_with_tax_premature(100.0, FlatRateTaxStrategy()) # => needs a strategy instance
simple = total_with_tax_simple(100.0) # => needs nothing extra
assert premature == simple == 108.0 # => removing the abstraction changed nothing observable
def test_the_simplified_version_has_strictly_less_indirection() -> None:
assert count_lines_of_indirection(strategy_based=True) > count_lines_of_indirection(strategy_based=False) # => the win
# => Run: pytest -q -- Output: 2 passedVerify: pytest -q
Output:
2 passedKey takeaway: OCP's indirection is only a win once a second variant actually needs to be swapped in; applied to a single, stable implementation it is pure ceremony that a reader has to see through to find the one formula that actually runs.
Why it matters: Premature abstraction is the mirror image of the god object -- instead of too little structure, it is structure built for requirements that were only ever speculative. Codebases accumulate Strategy, Factory, and Handler suffixes "for future flexibility" that never gets used, and each one is a permanent reading tax paid by every engineer who has to trace through it to find the one code path that actually executes.
Example 67: GoF Gallery -- Creational Patterns
ex-67 · exercises co-32, co-16, co-17, co-18, co-19
A single-file tour of the four essential creational patterns -- factory method, abstract factory, builder, and singleton -- each constructing correctly, verified independently in one gallery sweep.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC
graph LR
A["Factory Method<br/>shape_factory()"]:::blue
B["Abstract Factory<br/>DarkWidgetFactory"]:::orange
C["Builder<br/>RequestBuilder"]:::teal
D["Singleton<br/>AppConfig"]:::purple
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef purple fill:#CC78BC,stroke:#000000,color:#FFFFFF,stroke-width:2px
learning/code/ex-67-gof-gallery-creational/example.py
"""Example 67: GoF Gallery -- Creational Patterns.
co-32 (gof-pattern-gallery): a single-file tour of the four essential creational
patterns -- factory method (co-16), abstract factory (co-17), builder (co-18), and
singleton (co-19) -- each constructs correctly, verified independently.
"""
from __future__ import annotations # => defers type-hint evaluation for the forward references used below
from dataclasses import dataclass, field # => field() supplies mutable defaults safely, used by HttpRequest
# ============================================================
# 1. Factory Method -- defer instantiation to a creation method
# ============================================================
class Shape: # => the shared abstract product every factory-method output implements
def area(self) -> float: # => no body -- every concrete Shape must override this
raise NotImplementedError # => a plain-class stand-in for @abstractmethod, still enforces the contract
class Circle(Shape): # => one concrete product
def __init__(self, radius: float) -> None: # => the constructor
self.radius = radius # => stores radius on this instance
def area(self) -> float: # => satisfies Shape's contract
return 3.14159 * self.radius**2 # => pi * r^2
def shape_factory(kind: str, size: float) -> Shape: # => the FACTORY METHOD -- caller never names Circle directly
if kind == "circle": # => the ONLY place that knows which concrete class to build
return Circle(size) # => constructs the concrete product internally, hidden from the caller
raise ValueError(f"unknown shape kind: {kind}") # => an honest failure for any kind this factory cannot build
# ============================================================
# 2. Abstract Factory -- a family of factory methods producing a matched set
# ============================================================
@dataclass # => generates __init__ from the field below
class Button: # => one member of the "widget family"
theme: str # => the family tag, part of the generated __init__
@dataclass # => generates __init__ from the field below
class Checkbox: # => the other member of the "widget family" -- must match Button's theme
theme: str # => the family tag, part of the generated __init__
class WidgetFactory: # => the abstract factory interface
def create_button(self) -> Button: # => no body -- every concrete factory must override this
raise NotImplementedError # => a plain-class stand-in for @abstractmethod
def create_checkbox(self) -> Checkbox: # => no body -- every concrete factory must override this
raise NotImplementedError # => a plain-class stand-in for @abstractmethod
class DarkWidgetFactory(WidgetFactory): # => one concrete factory -- produces a MATCHED dark-themed family
def create_button(self) -> Button: # => overrides the abstract factory method
return Button(theme="dark") # => always builds the dark-family Button, never mixed with other themes
def create_checkbox(self) -> Checkbox: # => overrides the abstract factory method
return Checkbox(theme="dark") # => always builds the dark-family Checkbox, matching create_button()
# ============================================================
# 3. Builder -- assemble a complex object step by step
# ============================================================
@dataclass # => generates __init__ from the fields below
class HttpRequest: # => the complex object being assembled
url: str # => the one required field
headers: dict[str, str] = field(default_factory=dict) # => field() avoids a shared mutable default dict
body: str | None = None # => optional, defaults to no body
class RequestBuilder: # => the fluent builder -- no telescoping constructor needed
def __init__(self, url: str) -> None: # => the constructor takes only the one required part
self._url = url # => the one required part
self._headers: dict[str, str] = {} # => optional parts, added incrementally
self._body: str | None = None # => optional, starts unset until with_body() is called
def with_header(self, key: str, value: str) -> "RequestBuilder": # => returns self -- enables chaining
self._headers[key] = value # => accumulates one header per call, in-place
return self # => returning self is what makes .with_header(...).with_body(...) chainable
def with_body(self, body: str) -> "RequestBuilder": # => also returns self
self._body = body # => sets the optional body, overwriting any previous value
return self # => same chaining trick as with_header()
def build(self) -> HttpRequest: # => assembles the final immutable-in-spirit object
return HttpRequest(url=self._url, headers=self._headers, body=self._body) # => one call, fully assembled
# ============================================================
# 4. Singleton -- exactly one shared instance
# ============================================================
class AppConfig: # => a minimal singleton, deliberately small for this gallery tour (see Example 30 for the cost)
_instance: "AppConfig | None" = None # => the one shared instance, or None before first use
def __new__(cls) -> "AppConfig": # => overridden so every AppConfig() returns the SAME object
if cls._instance is None: # => first call: create the one instance
cls._instance = super().__new__(cls) # => allocate it exactly once
return cls._instance # => every subsequent call returns the SAME object
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
circle = shape_factory("circle", 2.0) # => 1. factory method
print(round(circle.area(), 2)) # => constructs a Circle without the caller importing Circle
# => Output: 12.57
dark_factory: WidgetFactory = DarkWidgetFactory() # => 2. abstract factory
print(dark_factory.create_button().theme, dark_factory.create_checkbox().theme) # => both match the same theme
# => Output: dark dark
# => 3. builder -- four chained calls assemble one HttpRequest, no telescoping constructor needed
request = RequestBuilder("https://api.example.com").with_header("Accept", "json").with_body("{}").build()
print(request.url, request.headers, request.body) # => 3. builder, assembled without a telescoping constructor
# => Output: https://api.example.com {'Accept': 'json'} {}
print(AppConfig() is AppConfig()) # => 4. singleton -- exactly one shared instance
# => Output: TrueRun: python3 example.py
Output:
12.57
dark dark
https://api.example.com {'Accept': 'json'} {}
Truelearning/code/ex-67-gof-gallery-creational/test_example.py
"""Example 67: pytest verification that each creational pattern constructs correctly."""
import pytest
from example import AppConfig, Circle, DarkWidgetFactory, RequestBuilder, shape_factory
def test_factory_method_constructs_the_correct_concrete_type() -> None:
shape = shape_factory("circle", 2.0) # => caller never named Circle directly
assert isinstance(shape, Circle) # => yet the correct concrete type was constructed
with pytest.raises(ValueError):
shape_factory("hexagon", 1.0) # => an unknown kind is rejected cleanly
def test_abstract_factory_produces_a_matched_family() -> None:
factory = DarkWidgetFactory()
button = factory.create_button()
checkbox = factory.create_checkbox()
assert button.theme == checkbox.theme == "dark" # => both members of the family share one theme
def test_builder_assembles_a_request_without_a_telescoping_constructor() -> None:
request = RequestBuilder("https://api.example.com").with_header("Accept", "json").with_body("{}").build()
assert request.url == "https://api.example.com" # => the required part
assert request.headers == {"Accept": "json"} # => an optional part, included
assert request.body == "{}" # => another optional part, included
def test_singleton_returns_the_same_instance_every_time() -> None:
assert AppConfig() is AppConfig() # => two calls, one object
# => Run: pytest -q -- Output: 4 passedVerify: pytest -q
Output:
4 passedKey takeaway: The four creational patterns answer different questions about "how does this object get built" -- one product from a name (factory method), a matched family (abstract factory), a complex object assembled step by step (builder), and exactly one shared instance (singleton) -- recognizing which question you're actually asking narrows the choice quickly.
Why it matters: Creation logic scattered across a codebase (direct ClassName() calls everywhere) is one of the most common sources of coupling to concrete implementations; centralizing it behind one of these four patterns is usually the cheapest, highest-leverage refactor available when a codebase needs to become more flexible about what gets constructed and how.
Example 68: GoF Gallery -- Structural Patterns
ex-68 · exercises co-32, co-20, co-21, co-22, co-23, co-24
A single-file tour of the five essential structural patterns -- adapter, decorator, facade, composite, and proxy -- each wraps correctly, verified independently in one gallery sweep.
learning/code/ex-68-gof-gallery-structural/example.py
"""Example 68: GoF Gallery -- Structural Patterns.
co-32 (gof-pattern-gallery): a single-file tour of the five essential structural
patterns -- adapter (co-20), decorator (co-21), facade (co-22), composite (co-23),
and proxy (co-24) -- each wraps correctly, verified independently.
"""
from __future__ import annotations # => defers type-hint evaluation for the forward references used below
from typing import Callable # => Callable types the logging_decorator's wrapped function argument
# ============================================================
# 1. Adapter -- convert one interface into another a client expects
# ============================================================
class FahrenheitSensor: # => the class being adapted -- exposes Fahrenheit only
def read_fahrenheit(self) -> float: # => the ONLY interface this class exposes
return 98.6 # => a fixed sample reading, in Fahrenheit
class CelsiusAdapter: # => converts FahrenheitSensor's interface into the one the client expects
def __init__(self, sensor: FahrenheitSensor) -> None: # => the constructor
self._sensor = sensor # => wraps the adapted object, held as a collaborator
def read_celsius(self) -> float: # => the interface the client actually wants
return (self._sensor.read_fahrenheit() - 32) * 5 / 9 # => the conversion, hidden from the client
# ============================================================
# 2. Decorator -- wrap an object to add behavior without subclass explosion
# ============================================================
def logging_decorator(func: Callable[[str], str]) -> Callable[[str], str]: # => wraps ANY str->str function
def wrapped(message: str) -> str: # => the replacement function returned in place of func
result = func(message) # => delegates to the wrapped function
return f"[logged] {result}" # => adds behavior around it, without editing func itself
return wrapped # => the decorator returns a NEW function, never mutates func in place
@logging_decorator # => applies the wrapper at definition time -- greet IS wrapped from here on
def greet(name: str) -> str: # => the wrapped function never knows it is being decorated
return f"hello, {name}" # => the original, undecorated behavior
# ============================================================
# 3. Facade -- one simplified entry point over a complex subsystem
# ============================================================
class Inventory: # => one subsystem member
def reserve(self, item: str) -> bool: # => the reservation step, hidden inside the facade
return True # => stubbed: assume reservation always succeeds
class Payment: # => another subsystem member
def charge(self, amount: float) -> bool: # => the charge step, hidden inside the facade
return True # => stubbed: assume payment always succeeds
class CheckoutFacade: # => hides the sequencing of Inventory + Payment behind ONE call
def __init__(self) -> None: # => the constructor
self._inventory = Inventory() # => wires subsystem one internally
self._payment = Payment() # => wires subsystem two internally
def checkout(self, item: str, amount: float) -> bool: # => the caller makes ONE call
return self._inventory.reserve(item) and self._payment.charge(amount) # => sequencing hidden inside
# ============================================================
# 4. Composite -- treat individual objects and compositions uniformly
# ============================================================
class FileNode: # => a leaf
def __init__(self, size: int) -> None: # => the constructor
self.size = size # => a leaf's own, fixed size
def total_size(self) -> int: # => the SHARED interface with DirectoryNode
return self.size # => a leaf's total IS its own size -- no recursion needed
class DirectoryNode: # => a composite -- contains other FileNode or DirectoryNode children
def __init__(self, children: list["FileNode | DirectoryNode"]) -> None: # => the constructor
self.children = children # => a mix of leaves and/or nested composites, held uniformly
def total_size(self) -> int: # => the SAME interface as FileNode -- callers don't special-case leaf vs. group
return sum(child.total_size() for child in self.children) # => recurses uniformly
# ============================================================
# 5. Proxy -- a stand-in controlling access to a real subject
# ============================================================
class ExpensiveImage: # => the "real subject", expensive to construct
def __init__(self, path: str) -> None: # => simulates a costly construction step
self.path = path # => stores the path on this instance
self.loaded = True # => simulates the expensive load happening at construction time
class LazyImageProxy: # => the proxy -- defers construction until first access
def __init__(self, path: str) -> None: # => cheap: no ExpensiveImage built yet
self._path = path # => remembered so the real subject can be built later, on demand
self._real: ExpensiveImage | None = None # => not loaded yet
def display(self) -> str: # => the client-facing interface, identical shape to using ExpensiveImage directly
if self._real is None: # => only construct the real subject on FIRST access
self._real = ExpensiveImage(self._path) # => the expensive construction, deferred until now
return f"displaying {self._real.path}" # => every later call reuses the already-built real subject
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
adapter = CelsiusAdapter(FahrenheitSensor()) # => 1. adapter
print(round(adapter.read_celsius(), 1)) # => the client reads Celsius, never touches Fahrenheit directly
# => Output: 37.0
print(greet("Ada")) # => 2. decorator -- logging added without editing greet()
# => Output: [logged] hello, Ada
facade = CheckoutFacade() # => 3. facade
print(facade.checkout("Book", 12.5)) # => one call hides inventory + payment sequencing
# => Output: True
tree = DirectoryNode([FileNode(10), DirectoryNode([FileNode(5), FileNode(3)])]) # => 4. composite
print(tree.total_size()) # => recursive total through ONE shared interface
# => Output: 18
proxy = LazyImageProxy("photo.png") # => 5. proxy
print(proxy._real is None) # => not loaded yet -- proves laziness
# => Output: True
print(proxy.display()) # => triggers the load, on first access only
# => Output: displaying photo.pngRun: python3 example.py
Output:
37.0
[logged] hello, Ada
True
18
True
displaying photo.pnglearning/code/ex-68-gof-gallery-structural/test_example.py
"""Example 68: pytest verification that each structural pattern wraps correctly."""
from example import (
CelsiusAdapter,
CheckoutFacade,
DirectoryNode,
FahrenheitSensor,
FileNode,
LazyImageProxy,
greet,
)
def test_adapter_converts_fahrenheit_to_celsius() -> None:
adapter = CelsiusAdapter(FahrenheitSensor())
assert round(adapter.read_celsius(), 1) == 37.0 # => the client reads Celsius only
def test_decorator_wraps_the_function_without_editing_it() -> None:
assert greet("Ada") == "[logged] hello, Ada" # => logging added purely by wrapping
def test_facade_hides_subsystem_sequencing_behind_one_call() -> None:
facade = CheckoutFacade()
assert facade.checkout("Book", 12.5) is True # => one call, inventory + payment both succeeded
def test_composite_computes_a_recursive_total_via_one_interface() -> None:
tree = DirectoryNode([FileNode(10), DirectoryNode([FileNode(5), FileNode(3)])])
assert tree.total_size() == 18 # => leaf and composite share total_size()
def test_proxy_defers_loading_until_first_access() -> None:
proxy = LazyImageProxy("photo.png")
assert proxy._real is None # => not loaded yet
proxy.display() # => triggers the load
assert proxy._real is not None # => loaded exactly once, on first access
# => Run: pytest -q -- Output: 5 passedVerify: pytest -q
Output:
5 passedKey takeaway: The five structural patterns all answer "how do objects compose" -- adapter bridges mismatched interfaces, decorator layers behavior, facade simplifies a subsystem's entry point, composite unifies leaf and group, and proxy stands in for a real subject -- distinct enough intents to recognize at a glance once you've seen each shape once.
Why it matters: Structural patterns are the vocabulary for "how do I connect these two things without coupling them permanently" -- a question that comes up constantly at integration boundaries (third-party APIs via adapter, cross-cutting concerns via decorator, subsystem entry points via facade). Recognizing which shape a given integration problem has speeds up the design conversation dramatically.
Example 69: GoF Gallery -- Behavioral Patterns
ex-69 · exercises co-32, co-25, co-26, co-27, co-28, co-29, co-30, co-31
A single-file tour of seven essential behavioral patterns -- strategy, observer, command, template method, state, iterator, and chain of responsibility -- each dispatches correctly, verified independently in one gallery sweep.
learning/code/ex-69-gof-gallery-behavioral/example.py
"""Example 69: GoF Gallery -- Behavioral Patterns.
co-32 (gof-pattern-gallery): a single-file tour of seven essential behavioral
patterns -- strategy (co-25), observer (co-26), command (co-27), template method
(co-28), state (co-29), iterator (co-30), and chain of responsibility (co-31) --
each dispatches correctly, verified independently.
"""
from __future__ import annotations # => defers type-hint evaluation for the forward references used below
from abc import ABC, abstractmethod # => builds the template-method, state, and chain-of-responsibility bases
from typing import Callable, Iterator # => Callable types strategies/observers, Iterator types the custom collection
# ============================================================
# 1. Strategy -- interchangeable algorithms behind one interface
# ============================================================
def by_length(word: str) -> int: # => one strategy
return len(word) # => a plain function IS a strategy -- no class hierarchy required
def sort_words(words: list[str], key: Callable[[str], int]) -> list[str]: # => the dispatcher, takes a strategy
return sorted(words, key=key) # => the strategy decides HOW to compare
# ============================================================
# 2. Observer -- subjects notify subscribers without knowing them concretely
# ============================================================
class Publisher: # => the subject
def __init__(self) -> None: # => the constructor
self._subscribers: list[Callable[[str], None]] = [] # => knows only that subscribers are callables
def subscribe(self, handler: Callable[[str], None]) -> None: # => adding a subscriber needs no publisher edit
self._subscribers.append(handler) # => grows the list, nothing else
def publish(self, message: str) -> None: # => notifies every subscriber, without knowing what they do
for handler in self._subscribers: # => visits every registered subscriber, in order
handler(message) # => the subject never knows what a handler does with the message
# ============================================================
# 3. Command -- reify a request as an object with execute/undo
# ============================================================
class AddTextCommand: # => a request, turned into an object
def __init__(self, document: list[str], text: str) -> None: # => the constructor
self.document = document # => the receiver this command acts on
self.text = text # => remembers WHAT it appended, needed later to undo
def execute(self) -> None: # => performs the request
self.document.append(self.text) # => the forward action
def undo(self) -> None: # => reverses the request
self.document.remove(self.text) # => the reverse action, mirroring execute() exactly
# ============================================================
# 4. Template Method -- a base defines the skeleton, subclasses fill steps
# ============================================================
class ReportBase(ABC): # => defines the FIXED skeleton
def run(self) -> str: # => the shared flow, defined exactly once
return f"[{self.header()}] {self.body()} [{self.footer()}]" # => calls the varying steps in a fixed order
@abstractmethod # => forces every subclass to fill this ONE varying step
def body(self) -> str: # => the ONLY step subclasses must fill
raise NotImplementedError # => abstract method body, never actually executed
def header(self) -> str: # => a step with a sensible default, overridable
return "REPORT" # => the shared default, used unless a subclass overrides it
def footer(self) -> str: # => another step with a sensible default
return "END" # => the shared default, used unless a subclass overrides it
class SalesReport(ReportBase): # => fills only the varying step
def body(self) -> str: # => overrides the ONE required step -- header() and footer() stay default
return "sales: 42 units" # => the only thing SalesReport ever needs to supply
# ============================================================
# 5. State -- represent states as objects so transitions are explicit
# ============================================================
class TrafficLightState(ABC): # => the shared state interface every concrete light color implements
@abstractmethod # => forces every concrete state to define its own legal next transition
def next(self) -> "TrafficLightState": # => returns the NEXT legal state
raise NotImplementedError # => abstract method body, never actually executed
class Red(TrafficLightState): # => one concrete state object, not a boolean flag
def next(self) -> TrafficLightState: # => encodes red's ONE legal transition
return Green() # => red -> green is the only legal move
class Green(TrafficLightState): # => a second concrete state object
def next(self) -> TrafficLightState: # => encodes green's ONE legal transition
return Yellow() # => green -> yellow
class Yellow(TrafficLightState): # => a third concrete state object, completing the cycle
def next(self) -> TrafficLightState: # => encodes yellow's ONE legal transition
return Red() # => yellow -> red, completing the cycle
# ============================================================
# 6. Iterator -- expose sequential access without revealing representation
# ============================================================
class EvenNumbers: # => a custom collection with hidden internal representation
def __init__(self, upper_bound: int) -> None: # => the constructor
self._upper_bound = upper_bound # => the only internal state -- callers never see this directly
def __iter__(self) -> Iterator[int]: # => the iterator protocol -- callers just use a for loop
current = 0 # => the iteration cursor, private to this generator
while current < self._upper_bound: # => stops once the bound is reached
yield current # => lazily yields the next even number
current += 2 # => advances the cursor by 2, staying on even numbers
# ============================================================
# 7. Chain of Responsibility -- pass a request along handlers until one handles it
# ============================================================
class Handler(ABC): # => the shared link interface every tier in the chain implements
def __init__(self) -> None: # => the constructor
self._next: "Handler | None" = None # => no successor linked yet -- set_next() wires it later
def set_next(self, handler: "Handler") -> "Handler": # => links this handler to the next one in the chain
self._next = handler # => stores the successor link
return handler # => returned so calls can chain: a.set_next(b).set_next(c)
def handle(self, level: int) -> str: # => tries to handle, or passes along
if self.can_handle(level): # => asks the CONCRETE subclass whether it can resolve this level
return self.resolve(level) # => yes -- resolve it here, the chain stops
if self._next is not None: # => not handled here -- pass to the next link
return self._next.handle(level) # => delegates, recursively, to the next handler in the chain
return "UNHANDLED" # => fell off the end of the chain
@abstractmethod # => forces every concrete handler to define its own eligibility check
def can_handle(self, level: int) -> bool:
raise NotImplementedError # => abstract method body, never actually executed
@abstractmethod # => forces every concrete handler to define its own resolution
def resolve(self, level: int) -> str:
raise NotImplementedError # => abstract method body, never actually executed
class TierOneSupport(Handler): # => the FIRST link in the chain
def can_handle(self, level: int) -> bool: # => tier 1's own eligibility rule
return level <= 1 # => only handles the lowest severity levels
def resolve(self, level: int) -> str: # => tier 1's own resolution
return "resolved by tier 1"
class TierTwoSupport(Handler): # => the SECOND link in the chain
def can_handle(self, level: int) -> bool: # => tier 2's own eligibility rule
return level <= 2 # => handles what tier 1 could not
def resolve(self, level: int) -> str: # => tier 2's own resolution
return "resolved by tier 2"
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
print(sort_words(["banana", "kiwi", "fig"], key=by_length)) # => 1. strategy
# => Output: ['fig', 'kiwi', 'banana']
events: list[str] = [] # => the list the observer's handler will append into
publisher = Publisher() # => 2. observer
publisher.subscribe(lambda msg: events.append(msg.upper())) # => registers a handler, no Publisher edit needed
publisher.publish("news") # => notifies every subscriber, this one included
print(events)
# => Output: ['NEWS']
doc: list[str] = [] # => the receiver AddTextCommand will mutate
cmd = AddTextCommand(doc, "hello") # => 3. command
cmd.execute() # => performs the request: appends "hello" to doc
cmd.undo() # => reverses the SAME request: removes "hello" from doc
print(doc)
# => Output: []
print(SalesReport().run()) # => 4. template method
# => Output: [REPORT] sales: 42 units [END]
light: TrafficLightState = Red() # => 5. state
light = light.next() # => transitions via a method call, never an if/elif on a string or int flag
print(type(light).__name__)
# => Output: Green
print(list(EvenNumbers(10))) # => 6. iterator
# => Output: [0, 2, 4, 6, 8]
tier_one: Handler = TierOneSupport() # => 7. chain of responsibility
tier_two = TierTwoSupport() # => the second link, not yet wired into the chain
tier_one.set_next(tier_two) # => wires tier_one -> tier_two
print(tier_one.handle(2)) # => tier 1 can't handle level 2, passes to tier 2
# => Output: resolved by tier 2Run: python3 example.py
Output:
['fig', 'kiwi', 'banana']
['NEWS']
[]
[REPORT] sales: 42 units [END]
Green
[0, 2, 4, 6, 8]
resolved by tier 2learning/code/ex-69-gof-gallery-behavioral/test_example.py
"""Example 69: pytest verification that each behavioral pattern dispatches correctly."""
from example import (
AddTextCommand,
EvenNumbers,
Green,
Publisher,
Red,
SalesReport,
TierOneSupport,
TierTwoSupport,
by_length,
sort_words,
)
def test_strategy_sorts_by_the_supplied_key() -> None:
assert sort_words(["banana", "kiwi", "fig"], key=by_length) == ["fig", "kiwi", "banana"] # => shortest first
def test_observer_notifies_a_subscriber_without_publisher_edits() -> None:
events: list[str] = []
publisher = Publisher()
publisher.subscribe(lambda msg: events.append(msg.upper())) # => registered without editing Publisher
publisher.publish("news")
assert events == ["NEWS"]
def test_command_undo_reverses_the_last_command() -> None:
doc: list[str] = []
cmd = AddTextCommand(doc, "hello")
cmd.execute()
assert doc == ["hello"]
cmd.undo()
assert doc == [] # => undo reversed exactly what execute() did
def test_template_method_shares_the_flow_and_fills_only_body() -> None:
assert SalesReport().run() == "[REPORT] sales: 42 units [END]" # => header/footer defaults, body overridden
def test_state_moves_red_to_green_via_next() -> None:
light = Red()
light = light.next()
assert isinstance(light, Green) # => the state pattern's transition, verified by type
def test_iterator_yields_even_numbers_lazily() -> None:
assert list(EvenNumbers(10)) == [0, 2, 4, 6, 8] # => a for-loop-compatible custom iterator
def test_chain_of_responsibility_falls_through_to_the_next_handler() -> None:
tier_one = TierOneSupport()
tier_two = TierTwoSupport()
tier_one.set_next(tier_two)
assert tier_one.handle(2) == "resolved by tier 2" # => tier 1 couldn't handle it, tier 2 did
# => Run: pytest -q -- Output: 7 passedVerify: pytest -q
Output:
7 passedKey takeaway: The behavioral patterns all answer "how do objects communicate and share responsibility" -- swap an algorithm (strategy), broadcast a change (observer), reify a request (command), share a flow while varying one step (template method), make transitions explicit (state), hide iteration mechanics (iterator), or pass work down a chain until someone claims it (chain of responsibility).
Why it matters: Behavioral patterns are the largest GoF family because "how do objects coordinate" is where most real design decisions live -- event systems, undo stacks, pluggable algorithms, and workflow engines in production code are almost always one of these seven shapes wearing different domain names.
Example 70: SOLID -- Full Order Engine
ex-70 · exercises co-01, co-02, co-03, co-04, co-05
All five SOLID principles applied together to one small order engine, each at its own seam: SRP splits pricing/persistence/receipts into three classes, OCP adds a discount without editing the dispatcher, LSP lets any DiscountStrategy substitute for another, ISP gives OrderService only the narrow Repository protocol it needs, and DIP has OrderService depend on that protocol rather than a concrete database class.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
A["OrderService"]:::blue -->|DIP: depends on| B["Repository protocol"]:::orange
A -->|delegates pricing| C["OrderCalculator"]:::teal
C -->|OCP/LSP: takes any| D["DiscountStrategy"]:::purple
B -.->|ISP: one narrow method| E["InMemoryRepository"]:::brown
A -->|SRP: separate class| F["ReceiptFormatter"]:::brown
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef purple fill:#CC78BC,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef brown fill:#CA9161,stroke:#000000,color:#FFFFFF,stroke-width:2px
learning/code/ex-70-solid-full-order-engine/example.py
"""Example 70: SOLID -- Full Order Engine.
co-01..co-05: all five SOLID principles applied together to one small order engine,
each at its own seam -- SRP splits pricing/persistence/receipts into three classes;
OCP adds a new discount without editing the dispatcher; LSP lets any DiscountStrategy
substitute for another without breaking OrderCalculator; ISP gives OrderService only
the narrow Repository protocol it needs, not a fat one; DIP has OrderService depend
on that protocol, not a concrete database class.
"""
from __future__ import annotations # => defers type-hint evaluation for the forward references used below
from dataclasses import dataclass, field # => field() supplies a safe mutable default for Order.items
from typing import Protocol # => Protocol declares the DiscountStrategy and Repository abstractions
# ============================================================
# SRP -- three classes, three reasons to change
# ============================================================
@dataclass # => generates __init__ from the field below
class Order: # => plain data: an order's line items
items: dict[str, float] = field(default_factory=dict) # => item name -> unit price
class DiscountStrategy(Protocol): # => OCP: new discounts are added as new classes, not new elif branches
def apply(self, subtotal: float) -> float: ... # => every strategy takes a subtotal, returns the discounted total
class NoDiscount: # => LSP: substitutable for any other DiscountStrategy -- never breaks a caller's expectations
def apply(self, subtotal: float) -> float: # => satisfies DiscountStrategy structurally
return subtotal # => no discount applied
class TenPercentOff: # => LSP: also substitutable -- honors the SAME contract (never returns MORE than subtotal)
def apply(self, subtotal: float) -> float: # => satisfies DiscountStrategy structurally
return subtotal * 0.90 # => a flat 10% off, still honors the "never exceeds subtotal" contract
# => OrderCalculator never imports InMemoryRepository or ReceiptFormatter -- it only knows discount strategies
class OrderCalculator: # => SRP: pricing, and only pricing -- depends on the DiscountStrategy ABSTRACTION (DIP)
def total(self, order: Order, discount: DiscountStrategy) -> float: # => OCP: adding a discount needs no edit here
subtotal = sum(order.items.values()) # => information-expert: Order holds items, this sums them
return discount.apply(subtotal) # => delegates the VARYING part to whichever strategy was passed in
class Repository(Protocol): # => ISP: OrderService depends on ONLY this narrow protocol, not a fat DB interface
def save(self, order_id: str, total: float) -> None: ... # => the ONE method OrderService actually calls
# => a real implementation might be Postgres or Redis -- OrderService would not need to change at all
class InMemoryRepository: # => a concrete, swappable implementation of the Repository protocol
def __init__(self) -> None: # => the constructor
self.saved: dict[str, float] = {} # => order_id -> total, standing in for a real database
def save(self, order_id: str, total: float) -> None: # => satisfies Repository structurally
self.saved[order_id] = total # => the ONE narrow responsibility this class has
class ReceiptFormatter: # => SRP: formatting, and only formatting -- its own reason to change, separate from pricing
def format(self, order_id: str, total: float) -> str: # => defines the format() method
return f"Order {order_id}: ${total:.2f}" # => a pure string-building step, no pricing or persistence here
# => this is the class DIP is really about: it depends on Repository (abstract), never on InMemoryRepository (concrete)
class OrderService: # => DIP: depends on the Repository ABSTRACTION, never on InMemoryRepository directly
def __init__(self, calculator: OrderCalculator, repository: Repository) -> None: # => both injected, not constructed
self._calculator = calculator # => the high-level policy owns these abstractions
self._repository = repository # => held as a collaborator, never constructed internally
def checkout(self, order_id: str, order: Order, discount: DiscountStrategy) -> float: # => the ONE orchestrating method
total = self._calculator.total(order, discount) # => delegates pricing
self._repository.save(order_id, total) # => delegates persistence, via the ABSTRACT protocol only
return total # => hands the computed total back to the caller
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
order = Order(items={"Book": 20.0, "Pen": 5.0}) # => subtotal = 25.0
calculator = OrderCalculator() # => constructs the pricing collaborator
repository = InMemoryRepository() # => constructs the persistence collaborator
service = OrderService(calculator, repository) # => DIP: injected, not hard-coded
total_no_discount = service.checkout("ord-1", order, NoDiscount()) # => LSP: NoDiscount substitutes cleanly
print(total_no_discount) # => the undiscounted subtotal
# => Output: 25.0
total_with_discount = service.checkout("ord-2", order, TenPercentOff()) # => OCP: swapped WITHOUT editing service
print(total_with_discount) # => the SAME checkout() method, a different discount object
# => Output: 22.5
formatter = ReceiptFormatter() # => SRP: a third, independent responsibility
print(formatter.format("ord-2", total_with_discount)) # => confirms formatting stays separate from pricing
# => Output: Order ord-2: $22.50
print(repository.saved) # => confirms persistence happened via the Repository protocol only
# => Output: {'ord-1': 25.0, 'ord-2': 22.5}
# => all five SOLID seams cooperated in this one run: SRP, OCP, LSP, ISP, and DIP each did real work aboveRun: python3 example.py
Output:
25.0
22.5
Order ord-2: $22.50
{'ord-1': 25.0, 'ord-2': 22.5}learning/code/ex-70-solid-full-order-engine/test_example.py
"""Example 70: pytest verification of each SOLID seam in the order engine."""
from example import (
InMemoryRepository,
NoDiscount,
Order,
OrderCalculator,
OrderService,
ReceiptFormatter,
TenPercentOff,
)
def test_srp_each_class_has_exactly_one_reason_to_change() -> None:
assert not hasattr(OrderCalculator(), "save") # => pricing never touches persistence
assert not hasattr(InMemoryRepository(), "total") # => persistence never touches pricing
assert not hasattr(ReceiptFormatter(), "apply") # => formatting never touches discounting
def test_ocp_a_new_discount_is_added_without_editing_order_calculator() -> None:
order = Order(items={"Book": 20.0, "Pen": 5.0})
calculator = OrderCalculator()
assert calculator.total(order, NoDiscount()) == 25.0
assert calculator.total(order, TenPercentOff()) == 22.5 # => new strategy, zero edits to OrderCalculator.total
def test_lsp_any_discount_strategy_substitutes_for_another_without_breaking_the_caller() -> None:
order = Order(items={"Book": 20.0})
calculator = OrderCalculator()
for discount in (NoDiscount(), TenPercentOff()): # => both honor "never return more than subtotal"
assert calculator.total(order, discount) <= 20.0 # => the contract every substitutable strategy must satisfy
def test_isp_order_service_depends_on_only_the_narrow_save_method() -> None:
repository = InMemoryRepository()
assert not hasattr(repository, "delete") and not hasattr(repository, "update") # => no fat interface exists here
assert hasattr(repository, "save") # => exactly the one method OrderService's Repository protocol needs
def test_dip_order_service_never_constructs_a_concrete_repository_itself() -> None:
order = Order(items={"Book": 20.0})
repository = InMemoryRepository() # => constructed OUTSIDE OrderService and injected in
service = OrderService(OrderCalculator(), repository)
total = service.checkout("ord-1", order, NoDiscount())
assert total == 20.0
assert repository.saved == {"ord-1": 20.0} # => persistence happened through the injected abstraction
# => Run: pytest -q -- Output: 5 passedVerify: pytest -q
Output:
5 passedKey takeaway: The five SOLID principles are not five separate checklists to apply in isolation -- in a real design they compose: SRP decides where a responsibility lives, DIP decides what abstraction the caller depends on, OCP/LSP together decide how that abstraction's implementations can grow, and ISP keeps each dependency narrow.
Why it matters: A single small order engine touching all five principles is exactly the shape of code most production services take -- pricing, persistence, and formatting concerns intertwined by default unless deliberately separated. Seeing all five seams in one file makes the principles' interdependence concrete instead of five disconnected rules memorized separately.
Example 71: GRASP -- Full Assignment
ex-71 · exercises co-06, co-07, co-08, co-09, co-10, co-11, co-12, co-13, co-14
All nine GRASP patterns assigned across one small library-checkout domain: Loan is the information expert for its own fee; Library is the creator of its Loans; LibraryController routes checkout requests so the UI never touches the domain directly; low coupling and indirection keep Library and the overdue notifier decoupled through a callback; Loan's methods stay high-cohesion; FeePolicy dispatches polymorphically; LoanRepository is a pure fabrication keeping Library IO-free; and the FeePolicy interface protects against variations in the fee rule.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
A["LibraryController<br/>(controller)"]:::blue --> B["Library<br/>(creator, low coupling)"]:::orange
B -->|creates| C["Loan<br/>(information expert, high cohesion)"]:::teal
C -->|depends on| D["FeePolicy<br/>(protected variations, polymorphism)"]:::purple
B -->|delegates persistence| E["LoanRepository<br/>(pure fabrication)"]:::brown
B -.->|indirection: callback list| F["overdue notifier"]:::brown
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef purple fill:#CC78BC,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef brown fill:#CA9161,stroke:#000000,color:#FFFFFF,stroke-width:2px
learning/code/ex-71-grasp-full-assignment/example.py
"""Example 71: GRASP -- Full Assignment.
co-06..co-14: all nine GRASP patterns assigned across one small library-checkout
domain -- information expert, creator, controller, low coupling, high cohesion,
polymorphism, pure fabrication, indirection, and protected variations -- each
pattern placed at the point in the domain that motivated it.
"""
from __future__ import annotations # => defers type-hint evaluation for the forward references used below
from dataclasses import dataclass # => Loan uses only plain fields, so only dataclass itself is needed here
from datetime import date, timedelta # => date drives due_date/overdue math, timedelta computes the loan period
from typing import Callable, Protocol # => Protocol declares FeePolicy and OverdueNotifier, both stable seams
# ============================================================
# 9. Protected Variations -- an unstable fee RULE wrapped behind a stable interface
# ============================================================
class FeePolicy(Protocol): # => the stable interface -- a fee-rule change never touches Loan
def daily_rate(self) -> float: ... # => the ONE method every fee rule must provide
class StandardFeePolicy: # => one concrete, swappable rule
def daily_rate(self) -> float: # => satisfies FeePolicy structurally
return 0.25 # => the standard-member daily rate
# ============================================================
# 6. Polymorphism -- dispatch on fee policy type, no if/elif type-switch anywhere
# ============================================================
class PremiumFeePolicy: # => a SECOND concrete rule -- Loan never branches on which one it got
def daily_rate(self) -> float: # => satisfies FeePolicy structurally, same shape as StandardFeePolicy
return 0.10 # => premium members pay a lower daily rate
# ============================================================
# 5. High Cohesion + 1. Information Expert -- Loan's methods all use ITS OWN fields
# ============================================================
@dataclass # => generates __init__ from the fields below
class Loan: # => holds due_date, so Loan is the natural INFORMATION EXPERT for lateness/fee math
book_title: str # => plain field, part of the generated __init__
borrower: str # => plain field, part of the generated __init__
due_date: date # => plain field, part of the generated __init__
fee_policy: FeePolicy # => co-14: depends on the STABLE interface, not a concrete policy class
def days_overdue(self, today: date) -> int: # => every method here reads ONLY this instance's own fields
return max(0, (today - self.due_date).days) # => high cohesion: no unrelated state touched
def fee(self, today: date) -> float: # => information-expert: Loan owns due_date, so Loan computes the fee
return self.days_overdue(today) * self.fee_policy.daily_rate() # => co-6: dispatches polymorphically
# ============================================================
# 8. Pure Fabrication -- LoanRepository is not a domain concept, invented for persistence
# ============================================================
class LoanRepository: # => co-12: a non-domain class that exists purely to keep Library IO-free
def __init__(self) -> None: # => the constructor
self._loans: list[Loan] = [] # => stands in for a real database table
def add(self, loan: Loan) -> None: # => the ONE write path
self._loans.append(loan) # => appended, never mutated after this
def all(self) -> list[Loan]: # => the ONE read path
return list(self._loans) # => returns a COPY, so callers cannot mutate internal state
# ============================================================
# 2. Creator -- Library aggregates Loans, so Library is the natural creator of one
# ============================================================
class OverdueNotifier(Protocol): # => co-13: the mediator's narrow interface
def __call__(self, loan: Loan, /) -> None: ... # => positional-only so a bare Callable[[Loan], None] matches structurally
class Library: # => co-9: LOW COUPLING -- Library never imports a concrete notification class
def __init__(self, repository: LoanRepository, on_overdue: OverdueNotifier | None = None) -> None: # => both injected
self._repository = repository # => co-12: depends on the pure fabrication, not raw persistence code
self._on_overdue: list[OverdueNotifier] = [] # => co-13: INDIRECTION -- a list of callbacks, not direct refs
if on_overdue is not None: # => the optional notifier is registered only if the caller supplied one
self._on_overdue.append(on_overdue) # => co-9: coupling is through a callable, not a concrete class
def checkout(self, book_title: str, borrower: str, fee_policy: FeePolicy) -> Loan: # => co-7: CREATOR
due_date = date.today() + timedelta(days=14) # => Library aggregates Loans, so it creates them (co-7)
loan = Loan(book_title, borrower, due_date, fee_policy) # => the natural creation point
self._repository.add(loan) # => delegated to the pure fabrication
return loan # => hands the newly created Loan back to the caller
def check_for_overdue(self, today: date) -> list[Loan]: # => scans every stored loan for lateness
overdue = [loan for loan in self._repository.all() if loan.days_overdue(today) > 0] # => filters via Loan itself
for loan in overdue: # => visits each overdue loan
for notify in self._on_overdue: # => co-13: Library talks only to the MEDIATOR list, never to a concrete notifier
notify(loan) # => co-9: no import of any concrete notification class anywhere in Library
return overdue # => hands the overdue subset back to the caller
# ============================================================
# 4. Controller -- routes the "checkout" event; the UI never touches Library directly
# ============================================================
class LibraryController: # => co-8: a dedicated coordinating object between UI events and the domain
def __init__(self, library: Library) -> None: # => the constructor
self._library = library # => the ONE domain collaborator the controller coordinates with
def handle_checkout_request(self, book_title: str, borrower: str, is_premium: bool) -> Loan: # => the ENTRY POINT
policy: FeePolicy = PremiumFeePolicy() if is_premium else StandardFeePolicy() # => co-6: chosen, not switched on later
return self._library.checkout(book_title, borrower, policy) # => the UI calls THIS, never Library directly
# ============================================================
# Wiring: an event log stands in for a real NotificationService, decoupled via co-13
# ============================================================
def make_logging_notifier(log: list[str]) -> Callable[[Loan], None]: # => co-9: Library only knows this is callable
def notify(loan: Loan) -> None: # => the actual callback, closing over `log`
log.append(f"{loan.borrower} is overdue on {loan.book_title}") # => the ONE side effect this notifier has
return notify # => a plain function IS a valid OverdueNotifier -- no class required
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
notifications: list[str] = [] # => the log the notifier closure above appends into
repository = LoanRepository() # => 8. pure fabrication
library = Library(repository, on_overdue=make_logging_notifier(notifications)) # => 9. low coupling + 13 indirection
controller = LibraryController(library) # => 4. controller
controller.handle_checkout_request("Clean Code", "Ada", is_premium=False) # => 2. creator, via Library.checkout
controller.handle_checkout_request("Refactoring", "Grace", is_premium=True) # => 6. polymorphism: different policy
past_due = date.today() + timedelta(days=20) # => simulate 6 days overdue
overdue_loans = library.check_for_overdue(past_due) # => triggers both the query and the mediated notification
print(len(overdue_loans)) # => both loans are overdue by this date
# => Output: 2
print(round(overdue_loans[0].fee(past_due), 2)) # => 1. information expert: Loan computes its own fee
# => Output: 1.5
print(round(overdue_loans[1].fee(past_due), 2)) # => 6. polymorphism: premium's LOWER daily rate applied
# => Output: 0.6
print(len(notifications)) # => 13. indirection worked: notifier fired without Library knowing its concrete type
# => Output: 2Run: python3 example.py
Output:
2
1.5
0.6
2learning/code/ex-71-grasp-full-assignment/test_example.py
"""Example 71: pytest verification that each of the nine GRASP patterns is placed correctly."""
from datetime import timedelta
from example import (
Library,
LibraryController,
LoanRepository,
PremiumFeePolicy,
StandardFeePolicy,
make_logging_notifier,
)
def test_information_expert_loan_computes_its_own_fee() -> None:
repository = LoanRepository()
library = Library(repository)
loan = library.checkout("Clean Code", "Ada", StandardFeePolicy()) # => Loan owns due_date, computes its own fee
later = loan.due_date + timedelta(days=4)
assert loan.fee(later) == 1.0 # => 4 days overdue * $0.25/day
def test_creator_library_creates_its_own_loans() -> None:
repository = LoanRepository()
library = Library(repository)
loan = library.checkout("Refactoring", "Grace", StandardFeePolicy())
assert loan in repository.all() # => the creation method lives on Library, not scattered elsewhere
def test_controller_routes_the_checkout_request_so_the_ui_never_touches_library_directly() -> None:
repository = LoanRepository()
library = Library(repository)
controller = LibraryController(library)
loan = controller.handle_checkout_request("Domain-Driven Design", "Eric", is_premium=True)
assert loan.book_title == "Domain-Driven Design" # => reached the domain only through the controller
def test_polymorphism_premium_and_standard_policies_dispatch_differently() -> None:
repository = LoanRepository()
library = Library(repository)
standard_loan = library.checkout("Book A", "Bob", StandardFeePolicy())
premium_loan = library.checkout("Book B", "Cate", PremiumFeePolicy())
later = standard_loan.due_date + timedelta(days=10)
assert standard_loan.fee(later) > premium_loan.fee(later) # => same days overdue, different rate, no type-switch
def test_pure_fabrication_repository_keeps_library_io_free() -> None:
repository = LoanRepository()
assert not hasattr(Library(repository), "_loans") # => Library never holds raw persistence state itself
def test_low_coupling_and_indirection_notifier_fires_without_a_concrete_reference() -> None:
log: list[str] = []
repository = LoanRepository()
library = Library(repository, on_overdue=make_logging_notifier(log)) # => Library only knows a callable exists
loan = library.checkout("Overdue Book", "Dan", StandardFeePolicy())
past_due = loan.due_date + timedelta(days=1)
library.check_for_overdue(past_due)
assert log == ["Dan is overdue on Overdue Book"] # => the mediator/callback fired, decoupled from any class name
def test_high_cohesion_loan_methods_touch_only_its_own_fields() -> None:
loan = Library(LoanRepository()).checkout("Book C", "Eve", StandardFeePolicy())
assert loan.days_overdue(loan.due_date) == 0 # => not yet due
assert loan.days_overdue(loan.due_date + timedelta(days=3)) == 3 # => uses only its own due_date
def test_protected_variations_swapping_the_fee_policy_needs_no_loan_edit() -> None:
repository = LoanRepository()
library = Library(repository)
loan = library.checkout("Book D", "Frank", PremiumFeePolicy()) # => a DIFFERENT policy, same Loan class
assert loan.fee_policy.daily_rate() == 0.10 # => the swap is transparent through the FeePolicy interface
# => Run: pytest -q -- Output: 8 passedVerify: pytest -q
Output:
8 passedKey takeaway: GRASP is not nine independent rules to memorize -- in a real domain they cluster around a small number of decisions: who owns the data (information expert, creator, high cohesion) and how collaborators talk to each other (low coupling, indirection, protected variations, polymorphism), with controller and pure fabrication as the two escape hatches for responsibilities that don't fit either camp.
Why it matters: A checkout-and-notify workflow like this one is one of the most common shapes in production software (e-commerce, library systems, subscription billing), and every GRASP decision made here recurs in each of those domains. Seeing all nine placed in one small, runnable example makes the underlying question each pattern answers ("who should know this? who should own this?") concrete rather than abstract.
Example 72: LSP -- Contract Test
ex-72 · exercises co-03
A single contract-test suite (run_stack_contract) that any Stack implementation must pass -- LIFO order, size tracking, and raising on pop-from-empty. A conforming ListStack passes it; a subtype that silently behaves like a queue (BuggyQueueAsStack) fails the SAME contract, with no separate test file needed per implementation.
learning/code/ex-72-lsp-contract-test/example.py
"""Example 72: LSP -- Contract Test.
co-03: a single contract-test suite that ANY Stack implementation must satisfy --
LIFO push/pop order, size tracking, and raising on pop-from-empty. A conforming
subtype passes every check; a subtype that violates the Liskov substitution
principle (silently behaving like a queue instead of a stack) fails the SAME
contract, without writing a separate test file per implementation.
"""
from __future__ import annotations # => defers type-hint evaluation, not strictly required here but kept for consistency
from typing import Protocol # => Protocol declares the structural contract every Stack implementation must satisfy
# => this Protocol is the SINGLE source of truth both implementations below are measured against
# => four clauses total: push, pop, is_empty, size -- run_stack_contract below exercises all four
class StackLike(Protocol): # => the contract every Stack implementation promises to honor
def push(self, item: int) -> None: ... # => clause: adds one item
def pop(self) -> int: ... # => clause: removes and returns the MOST RECENTLY pushed item
def is_empty(self) -> bool: ... # => clause: reports whether any items remain
def size(self) -> int: ... # => clause: reports how many items remain
# => ListStack and BuggyQueueAsStack both satisfy StackLike STRUCTURALLY -- only the contract test below catches the difference
class ListStack: # => a conforming implementation -- true last-in-first-out order
def __init__(self) -> None: # => the constructor
self._items: list[int] = [] # => the backing storage, empty at construction
def push(self, item: int) -> None: # => satisfies StackLike's push clause
self._items.append(item) # => appends to the END of the list -- pop() below removes from here too
def pop(self) -> int: # => satisfies StackLike's pop clause
if self.is_empty(): # => satisfies the "raises on pop-from-empty" clause of the contract
raise IndexError("pop from empty stack") # => an honest failure, never a silent wrong answer
return self._items.pop() # => removes and returns the LAST item pushed -- LIFO
def is_empty(self) -> bool: # => satisfies StackLike's is_empty clause
return len(self._items) == 0 # => true exactly when nothing has been pushed and not yet popped
def size(self) -> int: # => satisfies StackLike's size clause
return len(self._items) # => the current count of pushed-but-not-popped items
# => notice this class's method SIGNATURES are identical to ListStack's -- only pop()'s BEHAVIOR differs
class BuggyQueueAsStack: # => LSP VIOLATION: looks like a Stack, secretly behaves like a queue (FIFO)
def __init__(self) -> None: # => the constructor -- identical to ListStack's
self._items: list[int] = [] # => the backing storage, empty at construction
def push(self, item: int) -> None: # => IDENTICAL to ListStack.push -- the bug is not here
self._items.append(item) # => appends to the END of the list, same as ListStack
def pop(self) -> int: # => structurally satisfies StackLike's pop clause -- but VIOLATES its LIFO promise
if self.is_empty(): # => still raises on empty, so this clause alone would pass
raise IndexError("pop from empty stack") # => an honest failure for the empty case
return self._items.pop(0) # => BUG: removes the FIRST item -- FIFO order, not LIFO
def is_empty(self) -> bool: # => IDENTICAL to ListStack.is_empty
return len(self._items) == 0 # => same logic, same correctness
def size(self) -> int: # => IDENTICAL to ListStack.size
return len(self._items) # => same logic, same correctness
# => a type-level parameter, not an instance -- run_stack_contract works for ANY conforming class, unmodified
def run_stack_contract(make_stack: type[StackLike]) -> None: # => the ONE contract every subtype must pass
stack = make_stack() # => constructs whichever implementation was passed in, generically
# => the SAME four assertions below run for both ListStack and BuggyQueueAsStack -- only the outcome differs
assert stack.is_empty() # => clause 1: starts empty
stack.push(1) # => pushed first -- should be the LAST one popped, if LIFO holds
stack.push(2) # => pushed second
stack.push(3) # => pushed third and last -- should be the FIRST one popped, if LIFO holds
assert stack.size() == 3 # => clause 2: size tracks pushes
top = stack.pop() # => the value under test -- LIFO demands this be 3
assert top == 3, f"LSP violation: expected LIFO top 3, got {top}" # => clause 3: LIFO order
assert stack.size() == 2 # => clause 4: size tracks pops
# => reaching this line without an AssertionError means every clause of the contract held
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
# => no separate ListStackTest / BuggyQueueAsStackTest classes exist -- one contract function serves both
run_stack_contract(ListStack) # => the SAME contract function, applied to the conforming implementation
print("ListStack: contract satisfied") # => reached only if every assertion above passed
# => Output: ListStack: contract satisfied
try: # => catches the assertion failure so this demonstration can print it, instead of crashing
run_stack_contract(BuggyQueueAsStack) # => the SAME contract function, now applied to the buggy one
print("BuggyQueueAsStack: contract satisfied") # => unreached -- the LIFO assertion fails first
except AssertionError as error: # => catches the SAME contract's failure, without a separate test file
print(f"BuggyQueueAsStack: contract VIOLATED -- {error}") # => the LSP violation surfaces here
# => Output: BuggyQueueAsStack: contract VIOLATED -- LSP violation: expected LIFO top 3, got 1Run: python3 example.py
Output:
ListStack: contract satisfied
BuggyQueueAsStack: contract VIOLATED -- LSP violation: expected LIFO top 3, got 1learning/code/ex-72-lsp-contract-test/test_example.py
"""Example 72: pytest verification that the SAME contract passes ListStack and fails BuggyQueueAsStack."""
import pytest
from example import BuggyQueueAsStack, ListStack, run_stack_contract
def test_conforming_implementation_satisfies_the_stack_contract() -> None:
run_stack_contract(ListStack) # => no exception raised -- LSP holds: ListStack substitutes cleanly
def test_violating_subtype_fails_the_same_contract() -> None:
with pytest.raises(AssertionError, match="LSP violation"):
run_stack_contract(BuggyQueueAsStack) # => the SAME contract catches the violation, no new test file needed
def test_empty_stack_raises_on_pop_for_both_implementations() -> None:
for make_stack in (ListStack, BuggyQueueAsStack):
stack = make_stack()
with pytest.raises(IndexError):
stack.pop() # => both implementations honor this part of the contract correctly
# => Run: pytest -q -- Output: 3 passedVerify: pytest -q
Output:
3 passedKey takeaway: LSP is testable, not just a design opinion -- write the contract ONCE as a function, and run it against every subtype. A subtype that "compiles" but behaves differently under the same contract is exactly what LSP forbids.
Why it matters: Contract tests catch LSP violations automatically in CI, the moment a new subtype is added, instead of relying on a reviewer noticing a subtle behavioral difference during code review.
Example 73: ISP -- Protocol Decomposition
ex-73 · exercises co-04
A fat FatWorker protocol bundling print/scan/fax forces every implementer to support all three. Decomposing it into Printer, Scanner, and FaxMachine -- each a narrow Protocol -- lets a minimal BasicPrinter satisfy only Printer, structurally, with no unused-method stubs and no explicit inheritance declaration.
learning/code/ex-73-isp-protocol-decomposition/example.py
"""Example 73: ISP -- Protocol Decomposition.
co-04: a fat `Worker` service (print + scan + fax) forces every implementation to
stub methods it does not need. Decomposing it into fine-grained `Printer`,
`Scanner`, and `FaxMachine` protocols lets a minimal implementation (a
print-only device) satisfy only the protocol it actually needs, structurally --
no explicit inheritance, no unused-method stubs.
"""
from __future__ import annotations # => defers type-hint evaluation for the forward references used below
from typing import Protocol, runtime_checkable # => runtime_checkable enables isinstance() checks below
# ============================================================
# BEFORE: one fat protocol -- every implementer forced to support all three
# ============================================================
# => BEFORE: a print-only device would be FORCED to stub scan_document() and send_fax() just to satisfy this
class FatWorker(Protocol): # => the ISP violation: three unrelated responsibilities bundled into one contract
def print_document(self, name: str) -> str: ... # => any implementer must support this, even if print-only
def scan_document(self, name: str) -> str: ... # => forces a print-only device to stub this method too
def send_fax(self, name: str) -> str: ... # => forces a print-only device to stub this method as well
# ============================================================
# AFTER: three fine-grained protocols -- each implementer depends on only what it uses
# ============================================================
@runtime_checkable # => enables isinstance(obj, Printer) structural checks, used in the demonstration below
class Printer(Protocol): # => co-04: the narrow protocol for print-capable devices only
def print_document(self, name: str) -> str: ... # => the ONE method this narrow protocol requires
@runtime_checkable # => enables isinstance(obj, Scanner) structural checks
class Scanner(Protocol): # => a second, independent narrow protocol
def scan_document(self, name: str) -> str: ... # => the ONE method this narrow protocol requires
@runtime_checkable # => enables isinstance(obj, FaxMachine) structural checks
class FaxMachine(Protocol): # => a third, independent narrow protocol
def send_fax(self, name: str) -> str: ... # => the ONE method this narrow protocol requires
class BasicPrinter: # => a MINIMAL implementation -- satisfies Printer only, no stub methods needed
def print_document(self, name: str) -> str: # => satisfies Printer structurally
return f"printed: {name}" # => a real, honest implementation
class MultiFunctionDevice: # => satisfies all three protocols structurally -- no explicit inheritance declared
def print_document(self, name: str) -> str: # => satisfies Printer structurally
return f"printed: {name}" # => a real, honest implementation
def scan_document(self, name: str) -> str: # => satisfies Scanner structurally
return f"scanned: {name}" # => a real, honest implementation
def send_fax(self, name: str) -> str: # => satisfies FaxMachine structurally
return f"faxed: {name}" # => a real, honest implementation
def run_print_job(printer: Printer, name: str) -> str: # => depends on the NARROW Printer protocol only
return printer.print_document(name) # => works for BasicPrinter or MultiFunctionDevice, unchanged either way
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
basic = BasicPrinter() # => a print-only device, structurally
print(run_print_job(basic, "invoice.pdf")) # => a print-only device satisfies the narrow protocol
# => Output: printed: invoice.pdf
print(isinstance(basic, Printer)) # => structurally satisfies Printer
# => Output: True
print(isinstance(basic, Scanner)) # => correctly does NOT satisfy Scanner -- ISP kept the protocols apart
# => Output: False
mfd = MultiFunctionDevice() # => a device satisfying all three narrow protocols at once, still with zero inheritance
print(isinstance(mfd, Printer) and isinstance(mfd, Scanner) and isinstance(mfd, FaxMachine)) # => satisfies all three
# => Output: TrueRun: python3 example.py
Output:
printed: invoice.pdf
True
False
Truelearning/code/ex-73-isp-protocol-decomposition/test_example.py
"""Example 73: pytest verification that minimal implementations satisfy only the narrow protocols they need."""
from example import BasicPrinter, FaxMachine, MultiFunctionDevice, Printer, Scanner, run_print_job
def test_minimal_implementation_satisfies_only_the_printer_protocol() -> None:
basic = BasicPrinter()
assert isinstance(basic, Printer) # => structurally satisfies the narrow Printer protocol
assert not isinstance(basic, Scanner) # => correctly excluded -- never implemented scan_document
assert not isinstance(basic, FaxMachine) # => correctly excluded -- never implemented send_fax
def test_multi_function_device_satisfies_all_three_narrow_protocols() -> None:
mfd = MultiFunctionDevice()
assert isinstance(mfd, Printer)
assert isinstance(mfd, Scanner)
assert isinstance(mfd, FaxMachine) # => a richer device can still satisfy every narrow protocol at once
def test_a_function_depending_on_the_narrow_protocol_accepts_the_minimal_implementation() -> None:
result = run_print_job(BasicPrinter(), "invoice.pdf") # => run_print_job only ever needs Printer
assert result == "printed: invoice.pdf"
def test_a_function_depending_on_the_narrow_protocol_also_accepts_the_richer_implementation() -> None:
result = run_print_job(MultiFunctionDevice(), "invoice.pdf") # => ISP: the richer device fits the narrow need too
assert result == "printed: invoice.pdf"
# => Run: pytest -q -- Output: 4 passedVerify: pytest -q
Output:
4 passedKey takeaway: Protocol classes make ISP structural in Python -- a class satisfies a protocol by having the right methods, not by declaring inheritance, so decomposing a fat interface into narrow ones costs nothing at the implementation site.
Why it matters: The syllabus's original framing checks this decomposition with pyright; this sandbox cannot run pyright (see How verification works), so this example verifies the SAME structural claim at runtime with isinstance() against @runtime_checkable protocols instead -- the code is written to be pyright-clean in spirit even though no static checker actually ran here.
Example 74: DIP -- Hexagonal Ports
ex-74 · exercises co-05, co-14
Ports-and-adapters wiring a domain to infrastructure: OrderDomain depends only on the NotificationPort interface it defines itself; EmailAdapter and SmsAdapter are concrete adapters living outside the domain, swappable without touching OrderDomain's source.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
A["OrderDomain<br/>(depends only on the port)"]:::blue --> B["NotificationPort<br/>(interface, owned by domain)"]:::orange
C["EmailAdapter<br/>(infrastructure)"]:::teal -->|implements| B
D["SmsAdapter<br/>(infrastructure)"]:::purple -->|implements| B
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef purple fill:#CC78BC,stroke:#000000,color:#FFFFFF,stroke-width:2px
learning/code/ex-74-dip-hexagonal-ports/example.py
"""Example 74: DIP -- Hexagonal Ports.
co-05, co-14: ports-and-adapters (hexagonal architecture) wiring a domain to
infrastructure. The domain module defines a PORT (an abstract interface it
needs) and depends on nothing else; concrete ADAPTERS live in a separate
"infrastructure" namespace and implement the port. The domain's own source
never names an infrastructure class -- inspected here via OrderDomain's own
constructor annotation, not just asserted in prose.
"""
from __future__ import annotations # => defers type-hint evaluation for the forward references used below
from typing import Protocol # => Protocol declares the PORT the domain owns
# ============================================================
# DOMAIN -- defines the port it needs; imports nothing from infrastructure
# ============================================================
# => this Protocol lives IN the domain's own file/namespace -- infrastructure adapts to it, not the reverse
class NotificationPort(Protocol): # => the PORT: an interface OWNED by the domain, infra must conform to it
def send(self, message: str) -> None: ... # => the ONE method any adapter must provide
# => grep OrderDomain's source for "EmailAdapter" or "SmsAdapter" -- neither name appears anywhere in this class
class OrderDomain: # => co-05: depends on the ABSTRACT port, never on a concrete adapter class
def __init__(self, notifier: NotificationPort) -> None: # => the constructor names only the PORT type
self._notifier = notifier # => injected -- the domain never constructs its own infrastructure
# => this is OrderDomain's ONLY method that touches notification -- it goes through _notifier, never a class name
def confirm_order(self, order_id: str) -> None: # => the domain's own business method
self._notifier.send(f"order {order_id} confirmed") # => calls through the PORT only
# ============================================================
# INFRASTRUCTURE -- adapters that implement the domain's port; may import anything
# ============================================================
class EmailAdapter: # => a concrete ADAPTER living outside the domain namespace
def __init__(self) -> None: # => the constructor
self.sent: list[str] = [] # => stands in for a real SMTP client
def send(self, message: str) -> None: # => satisfies NotificationPort structurally
self.sent.append(f"[email] {message}") # => a real, honest implementation
class SmsAdapter: # => a SECOND adapter -- swappable without touching OrderDomain at all
def __init__(self) -> None: # => the constructor
self.sent: list[str] = [] # => stands in for a real SMS gateway
def send(self, message: str) -> None: # => satisfies NotificationPort structurally
self.sent.append(f"[sms] {message}") # => a real, honest implementation
def domain_module_imports_no_infrastructure_names() -> bool: # => co-05: verify the dependency direction for real
# => the true architectural check: OrderDomain's __init__ signature names only the PORT, never a concrete adapter
annotation = OrderDomain.__init__.__annotations__.get("notifier") # => reads the ACTUAL parameter annotation
return annotation in ("NotificationPort", NotificationPort) or str(annotation) == "NotificationPort" # => the real check
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
email = EmailAdapter() # => constructs one concrete adapter
domain = OrderDomain(email) # => wired at the OUTERMOST layer -- the domain itself never names EmailAdapter
domain.confirm_order("ord-1") # => the domain's own method, oblivious to WHICH adapter it holds
print(email.sent) # => confirms the email adapter recorded the message
# => Output: ['[email] order ord-1 confirmed']
sms = SmsAdapter() # => constructs a DIFFERENT concrete adapter
domain_with_sms = OrderDomain(sms) # => swap the adapter -- zero edits inside OrderDomain
domain_with_sms.confirm_order("ord-2") # => the SAME domain method, now routed through a different adapter
print(sms.sent) # => confirms the sms adapter recorded the message
# => Output: ['[sms] order ord-2 confirmed']
print(domain_module_imports_no_infrastructure_names()) # => confirms the dependency direction by inspection, not prose
# => Output: TrueRun: python3 example.py
Output:
['[email] order ord-1 confirmed']
['[sms] order ord-2 confirmed']
Truelearning/code/ex-74-dip-hexagonal-ports/test_example.py
"""Example 74: pytest verification of the hexagonal ports-and-adapters wiring."""
from example import (
EmailAdapter,
OrderDomain,
SmsAdapter,
domain_module_imports_no_infrastructure_names,
)
def test_domain_confirms_an_order_through_the_email_adapter() -> None:
email = EmailAdapter()
domain = OrderDomain(email)
domain.confirm_order("ord-1")
assert email.sent == ["[email] order ord-1 confirmed"]
def test_swapping_the_adapter_needs_no_edit_to_order_domain() -> None:
sms = SmsAdapter()
domain = OrderDomain(sms) # => same OrderDomain class, a DIFFERENT adapter -- zero source changes
domain.confirm_order("ord-2")
assert sms.sent == ["[sms] order ord-2 confirmed"]
def test_order_domain_depends_on_the_port_not_a_concrete_adapter() -> None:
assert domain_module_imports_no_infrastructure_names() # => co-05: the dependency direction points inward
# => Run: pytest -q -- Output: 3 passedVerify: pytest -q
Output:
3 passedKey takeaway: Hexagonal architecture is DIP applied at the module level -- the domain owns the port interface, and infrastructure adapters depend on the domain's port, not the other way around, so the domain can be tested and reused with zero knowledge of email, SMS, or any other delivery mechanism.
Why it matters: This is the shape behind most production "clean architecture" backends -- domain logic that never imports a database driver, an HTTP client, or a message queue library directly, making it trivial to swap infrastructure or run the domain in a test with an in-memory adapter.
Example 75: Observer -- Memory Leak, Fixed With weakref
ex-75 · exercises co-26
LeakySubject holds STRONG references to its observers, so an observer stays alive even after every other reference is dropped -- a classic Observer memory leak. WeakRefSubject stores observers in a weakref.WeakSet, letting an unsubscribed, otherwise-unreferenced observer be garbage-collected automatically.
learning/code/ex-75-observer-memory-leak/example.py
"""Example 75: Observer -- Memory Leak, Fixed With weakref.
co-26: the classic Observer memory leak -- a Subject holding STRONG references
to its observers keeps them alive forever, even after every other reference to
an observer is dropped, because the Subject itself is still a live referrer.
Switching the Subject's storage to `weakref.WeakSet` lets an unsubscribed,
otherwise-unreferenced observer be garbage-collected automatically.
"""
# => co-26 in one sentence: a Subject's own reference to an observer is a REAL reference, and real
# => references keep objects alive -- weakref.WeakSet trades that guarantee for automatic cleanup
from __future__ import annotations # => defers type-hint evaluation, not strictly required here but kept for consistency
import gc # => used to force collection deterministically for this demonstration
import weakref # => weakref.WeakSet is the fix -- membership that does not keep members alive
class Event: # => a minimal observer -- large in a real system, small here to keep the leak visible
def __init__(self, name: str) -> None: # => the constructor
self.name = name # => the only field, just enough to tell instances apart
def notify(self, message: str) -> None: # => the method any Subject would call on this observer
pass # => a real observer would react here; the leak does not depend on what notify() does
# => LeakySubject and WeakRefSubject expose the IDENTICAL public interface -- only storage differs
class LeakySubject: # => co-26 ANTI-PATTERN: strong references keep every observer alive forever
def __init__(self) -> None: # => the constructor
self._observers: list[Event] = [] # => a STRONG reference list -- the subject IS a live referrer
def subscribe(self, observer: Event) -> None: # => registers an observer, the leaky way
self._observers.append(observer) # => the subject now owns a strong reference
def observer_count(self) -> int: # => reports how many observers are still tracked
return len(self._observers) # => never shrinks on its own, even after the caller drops its reference
# => the fix is a ONE-LINE storage change (list -> WeakSet); subscribe() and observer_count() barely change
class WeakRefSubject: # => co-26 FIX: a WeakSet does not keep its members alive
def __init__(self) -> None: # => the constructor
self._observers: weakref.WeakSet[Event] = weakref.WeakSet() # => weak references only
def subscribe(self, observer: Event) -> None: # => registers an observer, the fixed way
self._observers.add(observer) # => no strong reference created -- the subject does not keep this alive
def observer_count(self) -> int: # => reports how many observers are still tracked
return len(self._observers) # => automatically shrinks once an observer is garbage-collected elsewhere
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
# => both halves below follow the SAME script: construct, subscribe, drop the caller's reference, collect, count
leaky = LeakySubject() # => constructs the leaky implementation
observer_a = Event("a") # => the caller's own reference to a new observer
leaky.subscribe(observer_a) # => the subject now ALSO holds a strong reference
del observer_a # => the CALLER dropped its reference -- but the subject still holds one
gc.collect() # => forces collection so the leak is visible immediately, not just eventually
print(leaky.observer_count()) # => LEAK: still 1 -- the strong reference kept it alive
# => Output: 1
# => the ONLY difference from here on is which Subject class is used -- same script, different result
# => this second half mirrors the first exactly, line for line, except the subject class used
fixed = WeakRefSubject() # => constructs the fixed implementation
observer_b = Event("b") # => the caller's own reference to a new observer
fixed.subscribe(observer_b) # => the subject holds only a WEAK reference this time
del observer_b # => the caller dropped its only OTHER reference
gc.collect() # => nothing else references the Event, so it is collected
print(fixed.observer_count()) # => FIXED: 0 -- weakref let it be garbage-collected
# => Output: 0
# => same subscribe/unsubscribe pattern, opposite outcome -- the storage type was the whole fixRun: python3 example.py
Output:
1
0learning/code/ex-75-observer-memory-leak/test_example.py
"""Example 75: pytest verification that weakref fixes the observer memory leak."""
import gc
from example import Event, LeakySubject, WeakRefSubject
def test_leaky_subject_keeps_an_unsubscribed_observer_alive() -> None:
subject = LeakySubject()
observer = Event("leak-me")
subject.subscribe(observer)
del observer # => caller drops its reference
gc.collect()
assert subject.observer_count() == 1 # => LEAK: the subject's strong reference kept it alive
def test_weakref_subject_lets_an_unreferenced_observer_be_collected() -> None:
subject = WeakRefSubject()
observer = Event("collect-me")
subject.subscribe(observer)
del observer # => no other strong reference exists anywhere
gc.collect()
assert subject.observer_count() == 0 # => FIXED: weakref did not keep it alive
def test_weakref_subject_keeps_a_still_referenced_observer() -> None:
subject = WeakRefSubject()
observer = Event("still-alive") # => held here -- a strong reference DOES still exist
subject.subscribe(observer)
gc.collect()
assert subject.observer_count() == 1 # => weakref only drops entries once ALL strong refs are gone
# => Run: pytest -q -- Output: 3 passedVerify: pytest -q
Output:
3 passedKey takeaway: weakref.WeakSet (or weakref.ref for a single reference) is Python's built-in fix for the Observer pattern's most common production bug -- a subject silently keeping every observer it has ever seen alive, growing memory usage unbounded over the process's lifetime.
Why it matters: This exact leak shows up in GUI frameworks, event buses, and pub/sub systems whenever a subject outlives the individual listeners registered against it -- a WeakSet (or an explicit unsubscribe()) is the two competing fixes, and the choice between them is itself a design decision worth making deliberately.
Example 76: Command -- Composite Macro Command With Grouped Undo
ex-76 · exercises co-27, co-23
A TextBuffer editor where every edit is a Command with do()/undo(). MacroCommand (co-23: composite -- a command made of commands) groups several edits into one atomic unit: its undo() reverses every sub-command in REVERSE order, so a multi-step edit undoes as a single step.
learning/code/ex-76-command-macro-undo/example.py
"""Example 76: Command -- Composite Macro Command With Grouped Undo.
co-27, co-23: a text-buffer editor where every edit is a Command object with
`do()`/`undo()`. A `MacroCommand` (co-23: composite -- a command made of
commands) groups several edits into one atomic unit: calling its `undo()`
reverses every sub-command in REVERSE order, so a multi-step edit undoes as a
single step, never leaving the buffer in a partially-undone state.
"""
from __future__ import annotations # => defers type-hint evaluation for the forward references used below
from typing import Protocol # => Protocol declares the Command shape every do/undo pair must satisfy
# => every Command below holds a reference to THIS SAME buffer -- there is only ever one receiver in this example
class TextBuffer: # => the receiver -- the object commands actually mutate
def __init__(self) -> None: # => the constructor
self.text = "" # => starts empty; every command below mutates THIS field
# => InsertCommand, DeleteLastCommand, and MacroCommand all satisfy THIS one shape, structurally
class Command(Protocol): # => co-27: every command supports do/undo symmetrically
def do(self) -> None: ... # => the forward action
def undo(self) -> None: ... # => the reverse action, mirroring do() exactly
# => InsertCommand and DeleteLastCommand are LEAVES in the composite sense -- MacroCommand is the COMPOSITE node
class InsertCommand: # => a concrete command: insert text at the end of the buffer
def __init__(self, buffer: TextBuffer, text: str) -> None: # => the constructor
self._buffer = buffer # => the receiver this command acts on
self._text = text # => remembers WHAT it inserted, needed later to undo
def do(self) -> None: # => satisfies Command's do() clause
self._buffer.text += self._text # => the forward action: appends the remembered text
def undo(self) -> None: # => satisfies Command's undo() clause
self._buffer.text = self._buffer.text[: -len(self._text)] # => removes exactly what was inserted
# => notice DeleteLastCommand needs internal state (_deleted) to undo -- InsertCommand needs none, it can recompute
# => remembering _deleted (not just _count) is what makes THIS command's undo() possible too
class DeleteLastCommand: # => a second concrete command: delete the last N characters
def __init__(self, buffer: TextBuffer, count: int) -> None: # => the constructor
self._buffer = buffer # => the receiver this command acts on
self._count = count # => how many trailing characters to remove
self._deleted = "" # => remembers what was deleted, so undo() can restore it exactly
def do(self) -> None: # => satisfies Command's do() clause
self._deleted = self._buffer.text[-self._count :] # => captures the characters BEFORE removing them
self._buffer.text = self._buffer.text[: -self._count] # => the forward action: removes the trailing slice
def undo(self) -> None: # => satisfies Command's undo() clause
self._buffer.text += self._deleted # => restores the exact characters that were removed
# => MacroCommand satisfies the SAME Command shape as its own children -- the composite pattern's defining trait
class MacroCommand: # => co-23: COMPOSITE -- a command built out of other commands, same Command interface
def __init__(self, commands: list[Command]) -> None: # => the constructor
self._commands = commands # => the ordered group of sub-commands this macro wraps
def do(self) -> None: # => satisfies Command's do() clause, delegating to every sub-command
for command in self._commands: # => forward order
command.do() # => each sub-command performs its own forward action
def undo(self) -> None: # => satisfies Command's undo() clause, delegating to every sub-command
for command in reversed(self._commands): # => co-27: REVERSE order -- undoes atomically as one unit
command.undo() # => each sub-command reverses its own forward action
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
buffer = TextBuffer() # => the shared receiver every command below will mutate
macro = MacroCommand( # => groups three sub-commands into one atomic unit
[ # => the ordered list handed to MacroCommand's constructor
InsertCommand(buffer, "hello "), # => sub-command 1: appends "hello "
InsertCommand(buffer, "world"), # => sub-command 2: appends "world"
DeleteLastCommand(buffer, 1), # => drops the trailing "d"
] # => the list itself, not yet executed -- macro.do() below runs it
) # => closes the sub-command list passed to MacroCommand
macro.do() # => runs all three sub-commands forward, as one grouped edit
print(repr(buffer.text)) # => shows the buffer after all three forward actions ran
# => Output: 'hello worl'
# => "hello " + "world" - "d" = "hello worl" -- three edits collapsed into one visible result
macro.undo() # => reverses all three, in REVERSE order, as one atomic undo
print(repr(buffer.text)) # => confirms the buffer is back to its original, pre-macro state
# => Output: ''
# => undoing in REVERSE order matters: undoing "insert world" before "insert hello " would corrupt the slice mathRun: python3 example.py
Output:
'hello worl'
''learning/code/ex-76-command-macro-undo/test_example.py
"""Example 76: pytest verification that MacroCommand groups undo atomically, in reverse order."""
from example import DeleteLastCommand, InsertCommand, MacroCommand, TextBuffer
def test_macro_do_runs_every_sub_command_forward_in_order() -> None:
buffer = TextBuffer()
macro = MacroCommand([InsertCommand(buffer, "hello "), InsertCommand(buffer, "world")])
macro.do()
assert buffer.text == "hello world"
def test_macro_undo_reverses_every_sub_command_in_reverse_order_as_one_atomic_step() -> None:
buffer = TextBuffer()
macro = MacroCommand(
[
InsertCommand(buffer, "hello "),
InsertCommand(buffer, "world"),
DeleteLastCommand(buffer, 1),
]
)
macro.do()
assert buffer.text == "hello worl"
macro.undo()
assert buffer.text == "" # => a SINGLE undo() call reversed all three steps, atomically
def test_undo_never_leaves_the_buffer_in_a_partially_undone_intermediate_state() -> None:
buffer = TextBuffer()
macro = MacroCommand([InsertCommand(buffer, "a"), InsertCommand(buffer, "b"), InsertCommand(buffer, "c")])
macro.do()
assert buffer.text == "abc"
macro.undo()
assert buffer.text == "" # => not "ab" or "a" -- the whole group undoes together, no partial state observed
# => Run: pytest -q -- Output: 3 passedVerify: pytest -q
Output:
3 passedKey takeaway: MacroCommand is the Composite pattern applied to Command -- because it implements the SAME do()/undo() interface as its children, callers cannot tell a macro from a single command, and grouped undo falls out of "reverse the list" with no special-case logic.
Why it matters: Every real editor, IDE, or design tool with a single undo button for a multi-step operation (find-and-replace-all, a drag that moves ten objects) uses exactly this composite-command shape.
Example 77: Strategy -- Registry-Driven Plugin System
ex-77 · exercises co-25, co-02
PricingStrategyRegistry maps a name to a PricingStrategy. Core code (checkout) looks a strategy up by name only -- it never imports a concrete strategy class. A third-party BlackFridayPricing strategy registers into the SAME registry from outside the core module, and checkout picks it up with zero core edits.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph LR
A["checkout()<br/>(core)"]:::blue --> B["PricingStrategyRegistry<br/>(name -> strategy)"]:::orange
C["StandardPricing<br/>(core, built-in)"]:::teal -->|registers into| B
D["BlackFridayPricing<br/>(third-party plugin)"]:::purple -->|registers into| B
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef purple fill:#CC78BC,stroke:#000000,color:#FFFFFF,stroke-width:2px
learning/code/ex-77-strategy-registry-plugin/example.py
"""Example 77: Strategy -- Registry-Driven Plugin System.
co-25, co-02: a `PricingStrategyRegistry` maps a string key to a
`PricingStrategy`. Core code (`checkout`) only ever looks a strategy up by
name -- it never imports a concrete strategy class. A THIRD-PARTY strategy
registers itself into the SAME registry from outside the core module and
`checkout` picks it up with zero edits to core code, satisfying OCP (co-02)
through Strategy (co-25).
"""
from __future__ import annotations # => defers type-hint evaluation for the forward references used below
from typing import Protocol # => Protocol declares the Strategy shape every pricing rule must satisfy
class PricingStrategy(Protocol): # => the Strategy interface -- every pricing rule implements this
def price(self, subtotal: float) -> float: ... # => the ONE method every pricing rule must provide
# => this dict IS the plugin seam: any code, core or third-party, can call register() at any time
class PricingStrategyRegistry: # => the plugin mechanism: a name -> strategy lookup, owned by core code
def __init__(self) -> None: # => the constructor
self._strategies: dict[str, PricingStrategy] = {} # => empty at construction, filled by register() calls
def register(self, name: str, strategy: PricingStrategy) -> None: # => any caller, core or third-party, can add
self._strategies[name] = strategy # => stores under the string key -- no import required by the registry
def get(self, name: str) -> PricingStrategy: # => looks up by NAME, never by class
if name not in self._strategies: # => an honest, explicit check before the lookup
raise KeyError(f"no pricing strategy registered under {name!r}") # => an honest failure for unknown names
return self._strategies[name] # => hands back whichever strategy was registered under this name
# ============================================================
# CORE -- ships with one built-in strategy; never imports anything third-party
# ============================================================
class StandardPricing: # => a built-in, core strategy
def price(self, subtotal: float) -> float: # => satisfies PricingStrategy structurally
return subtotal # => no discount at all -- the baseline
def checkout(registry: PricingStrategyRegistry, strategy_name: str, subtotal: float) -> float: # => core's ONE entry point
strategy = registry.get(strategy_name) # => core code looks up BY NAME -- no concrete strategy import here
return strategy.price(subtotal) # => delegates the VARYING part entirely to whichever strategy was found
# ============================================================
# THIRD-PARTY PLUGIN -- registers into the SAME registry, defined entirely outside core
# ============================================================
# => this class could live in an entirely separate package -- checkout() never imports it by name
class BlackFridayPricing: # => a plugin author's OWN class -- core never imports this
def price(self, subtotal: float) -> float: # => satisfies PricingStrategy structurally, same shape as core's
return subtotal * 0.60 # => an aggressive 40% off, defined entirely outside core
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
registry = PricingStrategyRegistry() # => the single shared registry both core and the plugin use
registry.register("standard", StandardPricing()) # => core registers its own built-in strategy
print(checkout(registry, "standard", 100.0)) # => resolves "standard" through the registry, not an import
# => Output: 100.0
registry.register("black-friday", BlackFridayPricing()) # => THIRD PARTY plugs in -- zero edits to checkout()
print(checkout(registry, "black-friday", 100.0)) # => the SAME checkout() function, a brand-new strategy name
# => Output: 60.0Run: python3 example.py
Output:
100.0
60.0learning/code/ex-77-strategy-registry-plugin/test_example.py
"""Example 77: pytest verification that a third-party strategy loads without editing checkout()."""
import pytest
from example import BlackFridayPricing, PricingStrategyRegistry, StandardPricing, checkout
def test_built_in_strategy_prices_at_face_value() -> None:
registry = PricingStrategyRegistry()
registry.register("standard", StandardPricing())
assert checkout(registry, "standard", 100.0) == 100.0
def test_third_party_strategy_registers_and_loads_without_a_core_edit() -> None:
registry = PricingStrategyRegistry()
registry.register("standard", StandardPricing())
registry.register("black-friday", BlackFridayPricing()) # => a plugin, added purely by calling register()
assert checkout(registry, "black-friday", 100.0) == 60.0 # => checkout() itself was never touched
def test_looking_up_an_unregistered_strategy_raises_a_clear_error() -> None:
registry = PricingStrategyRegistry()
with pytest.raises(KeyError, match="no pricing strategy registered"):
checkout(registry, "nonexistent", 100.0)
# => Run: pytest -q -- Output: 3 passedVerify: pytest -q
Output:
3 passedKey takeaway: A registry turns Strategy into a genuine plugin mechanism -- checkout() is closed for modification (OCP, co-02) because it only ever calls registry.get(name), so new strategies are pure additions, never edits.
Why it matters: This is the shape behind real plugin systems -- payment gateways, notification channels, export formats -- where a core team ships a stable registry and third parties add capabilities without ever touching the core repository.
Example 78: Pattern-or-Not -- A YAGNI Judgment Call, Across Three Scenarios
ex-78 · exercises co-34, co-02
Three scenarios, three different verdicts: a welcome-email formatter with no second variant planned skips Strategy; a checkout discount with three real variants today (and a fourth on the roadmap) earns Strategy; a one-off report converter used once skips a Converter interface. Each verdict rests on a concrete signal -- how many variations exist TODAY -- not a hunch.
learning/code/ex-78-pattern-vs-yagni-judgment/example.py
"""Example 78: Pattern-or-Not -- A YAGNI Judgment Call, Across Three Scenarios.
co-34, co-02: patterns are not free -- each one adds indirection that must earn
its keep. Three scenarios, three different verdicts, each justified by a
concrete signal (not a hunch): how many variations exist TODAY, and how likely
a second one really is.
"""
from __future__ import annotations # => defers type-hint evaluation, used only by Scenario 2's Protocol
from typing import Protocol # => Protocol declares DiscountStrategy -- used ONLY where the pattern is justified
# ============================================================
# Scenario 1: ONE email formatter, no second implementation planned -- SKIP the pattern
# ============================================================
def format_welcome_email(username: str) -> str: # => a PLAIN function -- no Strategy interface, no factory
# => Judgment: only one email format exists, and the product has no roadmap item for a second one.
# => Wrapping this in a Strategy pattern today buys zero flexibility and adds one more class to navigate.
return f"Welcome, {username}!" # => the entire behavior, in one line -- no interface needed to swap it later
# ============================================================
# Scenario 2: THREE discount rules today, a fourth confirmed on the roadmap -- USE Strategy
# ============================================================
class DiscountStrategy(Protocol): # => co-25: justified here -- 3 variants exist NOW, a 4th is already planned
def apply(self, subtotal: float) -> float: ... # => the ONE method every discount rule must provide
class NoDiscount: # => variant 1 of 3, already real today
def apply(self, subtotal: float) -> float: # => satisfies DiscountStrategy structurally
return subtotal # => no discount at all
class TenPercentOff: # => variant 2 of 3, already real today
def apply(self, subtotal: float) -> float: # => satisfies DiscountStrategy structurally
return subtotal * 0.90 # => a flat 10% off
class BuyOneGetOneFree: # => variant 3 of 3, already real today
def apply(self, subtotal: float) -> float: # => satisfies DiscountStrategy structurally
return subtotal * 0.50 # => an effective 50% off
def checkout_total(subtotal: float, discount: DiscountStrategy) -> float: # => the ONE dispatcher, edited zero times
# => Judgment: THREE real variants exist today (not hypothetical), and a fourth is already on the
# => roadmap. The indirection pays for itself immediately, not speculatively.
return discount.apply(subtotal) # => delegates the VARYING part to whichever strategy was passed in
# ============================================================
# Scenario 3: ONE-OFF internal report converter, used once, thrown away after -- SKIP the pattern
# ============================================================
def convert_report_to_csv_line(fields: list[str]) -> str: # => a PLAIN function -- no Converter interface
# => Judgment: this runs once, for one internal report, with a known lifetime of a single script run.
# => A pluggable Converter interface here is premature abstraction (ex-66) wearing a different hat.
return ",".join(fields) # => the entire behavior, in one line -- no interface needed for a one-off script
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
print(format_welcome_email("Ada")) # => scenario 1: a plain function, no pattern applied
# => Output: Welcome, Ada!
print(checkout_total(100.0, BuyOneGetOneFree())) # => scenario 2: Strategy, justified by 3+ real variants
# => Output: 50.0
print(convert_report_to_csv_line(["2026-07-17", "42", "ok"])) # => scenario 3: a plain function, no pattern applied
# => Output: 2026-07-17,42,okRun: python3 example.py
Output:
Welcome, Ada!
50.0
2026-07-17,42,oklearning/code/ex-78-pattern-vs-yagni-judgment/test_example.py
"""Example 78: pytest verification of all three pattern-or-not scenarios."""
from example import (
BuyOneGetOneFree,
NoDiscount,
TenPercentOff,
checkout_total,
convert_report_to_csv_line,
format_welcome_email,
)
def test_scenario_1_plain_function_is_sufficient_with_no_second_variant_in_sight() -> None:
assert format_welcome_email("Ada") == "Welcome, Ada!" # => no Strategy needed for a single, stable format
def test_scenario_2_strategy_pattern_earns_its_keep_with_three_real_variants_today() -> None:
assert checkout_total(100.0, NoDiscount()) == 100.0
assert checkout_total(100.0, TenPercentOff()) == 90.0
assert checkout_total(100.0, BuyOneGetOneFree()) == 50.0 # => three DISTINCT verdicts, justifying the interface
def test_scenario_3_plain_function_is_sufficient_for_a_one_off_throwaway_script() -> None:
assert convert_report_to_csv_line(["2026-07-17", "42", "ok"]) == "2026-07-17,42,ok" # => no Converter interface needed
# => Run: pytest -q -- Output: 3 passedVerify: pytest -q
Output:
3 passedKey takeaway: The YAGNI-vs-pattern decision is a count, not a feeling -- "how many real variants exist right now, and is a next one already committed" separates the cases where indirection pays for itself from the cases where it is decoration.
Why it matters: Premature abstraction (ex-66) and under-abstraction both cost real time; a repeatable, concrete test for "does this pattern earn its keep today" is more useful than either extreme's usual justification ("we might need it" vs. "keep it simple").
Example 79: Decorator -- functools Decorator vs. a GoF Class Decorator
ex-79 · exercises co-21
Python's @decorator syntax and the GoF Decorator pattern solve the SAME problem -- cross-cutting behavior without editing the wrapped object's source -- at different granularities: @log_calls wraps a single function; LoggingCalculatorDecorator wraps a whole object and forwards its interface.
learning/code/ex-79-decorator-python-native/example.py
"""Example 79: Decorator -- functools Decorator vs. a GoF Class Decorator.
co-21: Python's `@decorator` syntax and the GoF Decorator pattern solve the
SAME problem -- adding cross-cutting behavior without editing the wrapped
object's source -- but at different granularities. A `functools`-based
FUNCTION decorator wraps a single callable; a GoF-style CLASS decorator wraps
an OBJECT and forwards the rest of its interface, useful when you need to wrap
something that is not a bare function (e.g. an object with several methods).
"""
from __future__ import annotations # => defers type-hint evaluation for the forward references used below
import functools # => functools.wraps preserves the wrapped function's identity through decoration
from typing import Callable, Protocol # => Callable types the function decorator, Protocol types the class one
# ============================================================
# Pythonic native: a functools-based FUNCTION decorator
# ============================================================
# => log_calls returns a brand-new function -- add() below never has its own source code touched
def log_calls(func: Callable[..., float]) -> Callable[..., float]: # => wraps a single callable
@functools.wraps(func) # => preserves func.__name__/__doc__ -- good decorator hygiene
def wrapper(*args: float, **kwargs: float) -> float: # => the replacement function returned in place of func
result = func(*args, **kwargs) # => delegates to the wrapped function, unchanged
wrapper.calls.append((args, result)) # type: ignore[attr-defined] # => records every call, cross-cutting
return result # => the SAME result the wrapped function would have returned
wrapper.calls = [] # type: ignore[attr-defined] # => a log attached to the wrapper itself
return wrapper # => a NEW callable, never mutates func in place
@log_calls # => the native, idiomatic way to add cross-cutting behavior to ONE function
def add(a: float, b: float) -> float: # => the wrapped function, unaware it is being decorated
return a + b # => the original, undecorated behavior
# ============================================================
# GoF style: a CLASS decorator wrapping a whole object, forwarding its interface
# ============================================================
class Calculator(Protocol): # => the interface a GoF class decorator must preserve
def compute(self, a: float, b: float) -> float: ... # => the ONE method any Calculator must provide
class PlainAdder: # => the object being decorated -- has state and MULTIPLE potential methods, not just one function
def compute(self, a: float, b: float) -> float: # => satisfies Calculator structurally
return a + b # => the original, undecorated behavior
class LoggingCalculatorDecorator: # => co-21: GoF Decorator -- wraps an OBJECT, forwards its interface
def __init__(self, wrapped: Calculator) -> None: # => the constructor
self._wrapped = wrapped # => the object being decorated, held as a collaborator
self.calls: list[tuple[float, float, float]] = [] # => a log attached to this decorator instance
def compute(self, a: float, b: float) -> float: # => forwards the SAME interface Calculator declares
result = self._wrapped.compute(a, b) # => delegates to the wrapped object -- same interface preserved
self.calls.append((a, b, result)) # => the cross-cutting behavior: logging every call
return result # => the SAME result the wrapped object would have returned
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
print(add(2, 3)) # => the function decorator runs transparently
# => Output: 5
print(add.calls) # type: ignore[attr-defined] # => cross-cutting logging attached without touching add()'s own body
# => Output: [((2, 3), 5)]
decorated_calculator = LoggingCalculatorDecorator(PlainAdder()) # => wraps a WHOLE object, not a bare function
print(decorated_calculator.compute(4, 5)) # => the class decorator also runs transparently
# => Output: 9
print(decorated_calculator.calls) # => same cross-cutting idea, applied to a whole OBJECT instead of one function
# => Output: [(4, 5, 9)]Run: python3 example.py
Output:
5
[((2, 3), 5)]
9
[(4, 5, 9)]learning/code/ex-79-decorator-python-native/test_example.py
"""Example 79: pytest verification that both decorator styles add the same cross-cutting behavior."""
from example import LoggingCalculatorDecorator, PlainAdder, add
def test_functools_decorator_preserves_the_wrapped_functions_result() -> None:
assert add(2, 3) == 5
def test_functools_decorator_adds_cross_cutting_logging_without_touching_add_body() -> None:
add(10, 20)
assert (10, 20) in [call_args for call_args, _ in add.calls] # type: ignore[attr-defined] # => the log recorded the call
def test_functools_wraps_preserves_the_original_function_name() -> None:
assert add.__name__ == "add" # => good decorator hygiene: functools.wraps kept identity intact
def test_gof_class_decorator_preserves_the_wrapped_objects_result() -> None:
decorated = LoggingCalculatorDecorator(PlainAdder())
assert decorated.compute(4, 5) == 9
def test_gof_class_decorator_adds_cross_cutting_logging_to_a_whole_object() -> None:
decorated = LoggingCalculatorDecorator(PlainAdder())
decorated.compute(1, 1)
decorated.compute(2, 2)
assert decorated.calls == [(1, 1, 2), (2, 2, 4)] # => same cross-cutting idea, at object granularity
# => Run: pytest -q -- Output: 5 passedVerify: pytest -q
Output:
5 passedKey takeaway: The two forms are the same idea at different scopes -- a function decorator is a Decorator pattern specialized to callables, and Python's @ syntax is just sugar over "pass the function to another function and use the result."
Why it matters: Knowing when to reach for @functools.wraps-style decoration (logging, caching, retries around a single function) versus a full class decorator (wrapping a multi-method object like a repository or client) avoids over-engineering a one-function problem into a class hierarchy.
Example 80: Clean Design Preview -- Strategy + Factory + Observer + Decorator, Under SOLID
ex-80 · exercises co-02, co-25, co-16, co-26, co-21
A small order/pricing engine combining four patterns cohesively: Strategy for pluggable pricing, a factory function for choosing the right strategy, Observer for order-placed events, and a Decorator that adds logging without editing PricingEngine's own source -- the whole system extends without editing any closed class, previewing this topic's capstone.
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
A["LoggingPricingEngine<br/>(Decorator)"]:::brown --> B["PricingEngine<br/>(closed for modification)"]:::blue
B -->|delegates pricing| C["PricingStrategy<br/>(Strategy)"]:::orange
D["make_pricing_strategy()<br/>(Factory)"]:::teal -->|creates| C
B -->|notifies| E["OrderListener<br/>(Observer)"]:::purple
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef purple fill:#CC78BC,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef brown fill:#CA9161,stroke:#000000,color:#FFFFFF,stroke-width:2px
learning/code/ex-80-clean-design-preview/example.py
"""Example 80: Clean Design Preview -- Strategy + Factory + Observer + Decorator, Under SOLID.
co-02, co-25, co-16, co-26, co-21: a small order/pricing engine combining four
patterns cohesively -- Strategy (co-25) for pluggable pricing rules, a factory
function (co-16) for choosing the right strategy, Observer (co-26) for
order-placed events, and a Decorator (co-21) that adds logging around pricing
without editing PricingEngine's own source. The whole system extends (new
pricing rule, new listener) without editing any CLOSED class -- OCP (co-02) in
practice, previewing the shape of this topic's capstone.
"""
from __future__ import annotations # => defers type-hint evaluation for the forward references used below
from typing import Protocol # => Protocol declares each pattern's interface structurally, no explicit inheritance
# ============================================================
# Strategy (co-25) -- pluggable pricing rules
# ============================================================
class PricingStrategy(Protocol): # => the Strategy interface -- every pricing rule implements this
def price(self, subtotal: float) -> float: ... # => the ONE method every pricing rule must provide
class StandardPricing: # => variant 1: no discount
def price(self, subtotal: float) -> float: # => satisfies PricingStrategy structurally
return subtotal # => the baseline, no discount applied
class MemberDiscountPricing: # => variant 2: a member-only discount
def price(self, subtotal: float) -> float: # => satisfies PricingStrategy structurally
return subtotal * 0.85 # => a flat 15% off for members
# ============================================================
# Factory (co-16) -- centralizes which strategy applies, so callers never branch on membership
# ============================================================
def make_pricing_strategy(is_member: bool) -> PricingStrategy: # => co-16: a factory function, not a class switch
return MemberDiscountPricing() if is_member else StandardPricing() # => the ONE place membership is decided
# ============================================================
# Observer (co-26) -- order-placed events, decoupled from the pricing engine itself
# ============================================================
class OrderListener(Protocol): # => the Observer interface -- every listener implements this
def on_order_placed(self, total: float) -> None: ... # => the ONE callback every listener must provide
class ReceiptPrinter: # => a concrete listener -- the engine never imports this class by name
def __init__(self) -> None: # => the constructor
self.receipts: list[str] = [] # => this listener's own log, populated only by on_order_placed
def on_order_placed(self, total: float) -> None: # => satisfies OrderListener structurally
self.receipts.append(f"receipt: ${total:.2f}") # => this listener's OWN reaction to the event
class PricingEngine: # => co-02: CLOSED for modification -- new listeners/strategies never require editing this
def __init__(self) -> None: # => the constructor
self._listeners: list[OrderListener] = [] # => an OPEN list of observers, populated via add_listener()
def add_listener(self, listener: OrderListener) -> None: # => co-26: any listener can be registered, unnamed here
self._listeners.append(listener) # => the engine never knows or cares WHAT the listener does
def place_order(self, subtotal: float, strategy: PricingStrategy) -> float: # => the ONE orchestrating method
total = strategy.price(subtotal) # => co-25: delegates the varying part to the strategy
for listener in self._listeners: # => co-26: notifies every registered observer, none hard-coded
listener.on_order_placed(total) # => each listener reacts independently, engine stays ignorant of how
return total # => hands the computed total back to the caller
# ============================================================
# Decorator (co-21) -- wraps the engine with logging, no edits inside PricingEngine
# ============================================================
class LoggingPricingEngine: # => co-21: adds cross-cutting logging around the CLOSED PricingEngine
def __init__(self, wrapped: PricingEngine) -> None: # => the constructor
self._wrapped = wrapped # => the CLOSED engine being decorated, held as a collaborator
self.log: list[float] = [] # => the cross-cutting log this decorator adds
def add_listener(self, listener: OrderListener) -> None: # => forwards to the wrapped engine, same interface
self._wrapped.add_listener(listener) # => delegates -- the decorator adds no new listener behavior here
def place_order(self, subtotal: float, strategy: PricingStrategy) -> float: # => forwards, then logs
total = self._wrapped.place_order(subtotal, strategy) # => delegates the REAL work to the wrapped engine
self.log.append(total) # => logging bolted on from OUTSIDE -- PricingEngine's source untouched
return total # => the SAME result the wrapped engine would have returned
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
engine = LoggingPricingEngine(PricingEngine()) # => the CLOSED engine, wrapped in logging from the outside
receipts = ReceiptPrinter() # => a concrete observer, unknown to PricingEngine by name
engine.add_listener(receipts) # => registers the observer through the decorator, which forwards it
total_standard = engine.place_order(100.0, make_pricing_strategy(is_member=False)) # => strategy 1, via the factory
print(total_standard) # => confirms the standard pricing rule ran, unmodified
# => Output: 100.0
total_member = engine.place_order(100.0, make_pricing_strategy(is_member=True)) # => strategy 2, via the factory
print(total_member) # => confirms the member discount rule ran, unmodified
# => Output: 85.0
print(receipts.receipts) # => confirms the observer reacted to BOTH order-placed events
# => Output: ['receipt: $100.00', 'receipt: $85.00']
print(engine.log) # => the decorator logged both totals without PricingEngine knowing it was wrapped
# => Output: [100.0, 85.0]Run: python3 example.py
Output:
100.0
85.0
['receipt: $100.00', 'receipt: $85.00']
[100.0, 85.0]learning/code/ex-80-clean-design-preview/test_example.py
"""Example 80: pytest verification that the system extends without editing any closed class."""
from example import (
LoggingPricingEngine,
MemberDiscountPricing,
PricingEngine,
ReceiptPrinter,
StandardPricing,
make_pricing_strategy,
)
def test_factory_chooses_the_correct_strategy_by_membership() -> None:
assert isinstance(make_pricing_strategy(is_member=True), MemberDiscountPricing)
assert isinstance(make_pricing_strategy(is_member=False), StandardPricing)
def test_engine_notifies_registered_listeners_on_every_order() -> None:
engine = PricingEngine()
receipts = ReceiptPrinter()
engine.add_listener(receipts)
engine.place_order(100.0, StandardPricing())
assert receipts.receipts == ["receipt: $100.00"]
def test_a_second_listener_registers_without_editing_pricing_engine() -> None:
engine = PricingEngine()
first = ReceiptPrinter()
second = ReceiptPrinter()
engine.add_listener(first)
engine.add_listener(second) # => a SECOND listener, zero edits to PricingEngine's source
engine.place_order(50.0, StandardPricing())
assert first.receipts == ["receipt: $50.00"]
assert second.receipts == ["receipt: $50.00"]
def test_decorator_logs_every_total_without_touching_pricing_engine() -> None:
logging_engine = LoggingPricingEngine(PricingEngine())
logging_engine.place_order(100.0, StandardPricing())
logging_engine.place_order(100.0, MemberDiscountPricing())
assert logging_engine.log == [100.0, 85.0] # => decorator observed both, from OUTSIDE the closed class
def test_full_stack_strategy_factory_observer_decorator_work_together() -> None:
logging_engine = LoggingPricingEngine(PricingEngine())
receipts = ReceiptPrinter()
logging_engine.add_listener(receipts)
total = logging_engine.place_order(200.0, make_pricing_strategy(is_member=True))
assert total == 170.0
assert receipts.receipts == ["receipt: $170.00"]
assert logging_engine.log == [170.0]
# => Run: pytest -q -- Output: 5 passedVerify: pytest -q
Output:
5 passedKey takeaway: Four patterns compose cleanly here because each answers a DIFFERENT question -- Strategy answers "which pricing rule," the factory answers "which strategy to pick," Observer answers "who else needs to know," and Decorator answers "how do I add a cross-cutting concern from outside" -- no single class carries more than one of those responsibilities.
Why it matters: This is a direct preview of the topic's capstone: the same four-pattern combination, applied to a smellier starting point and driven through a full refactor under a pinning test suite.
State machines & statecharts
Example 81: Transition-Table FSM -- Order Lifecycle
ex-81 · exercises co-35
An order lifecycle (created → paid → shipped → delivered, with cancellation from created or paid) modeled as an explicit state × event → state transition table -- a plain dict, not a chain of if statements. Every illegal event in a state is rejected by the table itself, by omission.
stateDiagram-v2
[*] --> created
created --> paid: pay
created --> cancelled: cancel
paid --> shipped: ship
paid --> cancelled: cancel
shipped --> delivered: deliver
delivered --> [*]
cancelled --> [*]
learning/code/ex-81-transition-table-order-lifecycle/example.py
"""Example 81: Transition-Table FSM -- Order Lifecycle.
co-35: an order lifecycle (created -> paid -> shipped -> delivered, with
cancellation from created or paid) modeled as an explicit
state x event -> state TRANSITION TABLE -- a plain dict, not a chain of `if`
statements. Every illegal event in a state is rejected by the TABLE ITSELF
(a missing key), so "what can happen next" is one data structure you can read
top to bottom, not logic scattered across methods.
"""
from __future__ import annotations # => defers type-hint evaluation for the dict[tuple[str, str], str] alias below
class IllegalTransition(Exception): # => raised when the table has no entry for (state, event)
pass # => a plain marker exception -- no extra fields needed, the message carries the detail
# ============================================================
# The transition table -- the ENTIRE lifecycle logic lives in this one dict
# ============================================================
# => co-35: this dict IS the state machine -- reading it top to bottom answers "what can happen next"
ORDER_TRANSITIONS: dict[tuple[str, str], str] = { # => keys are (current_state, event) pairs, values are next states
("created", "pay"): "paid", # => created -> paid, on the "pay" event
("created", "cancel"): "cancelled", # => created -> cancelled, on the "cancel" event
("paid", "ship"): "shipped", # => paid -> shipped, on the "ship" event
("paid", "cancel"): "cancelled", # => paid -> cancelled, on the "cancel" event
("shipped", "deliver"): "delivered", # => shipped -> delivered, on the "deliver" event
# => no ("shipped", "cancel") entry -- a shipped order can no longer be cancelled, by OMISSION
# => no ("delivered", *) entries -- delivered is terminal, by omission
# => no ("cancelled", *) entries -- cancelled is terminal, by omission
} # => closes the table -- every legal transition in the entire lifecycle is visible above, nothing hidden elsewhere
class OrderFsm: # => a thin wrapper around the table -- holds no transition logic of its own
def __init__(self) -> None: # => the constructor
self.state = "created" # => every order starts in this one state
def send(self, event: str) -> str: # => the ONE method that ever changes self.state
key = (self.state, event) # => builds the (state, event) lookup key for the table
if key not in ORDER_TRANSITIONS: # => the TABLE rejects illegal events -- no scattered if-checks anywhere
raise IllegalTransition(f"event {event!r} is illegal in state {self.state!r}") # => an honest, specific failure
self.state = ORDER_TRANSITIONS[key] # => the table itself supplies the next state, no branching logic here
return self.state # => hands back the new state to the caller
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
order = OrderFsm() # => starts in "created", per __init__
print(order.send("pay")) # => created -> paid, a table lookup
# => Output: paid
print(order.send("ship")) # => paid -> shipped
# => Output: shipped
print(order.send("deliver")) # => shipped -> delivered
# => Output: delivered
try: # => attempts an event that the table has no entry for
order.send("cancel") # => illegal: delivered has no "cancel" entry in the table
except IllegalTransition as error: # => the table's own omission produced this, not a manual if-check
print(error) # => shows the honest, specific failure message
# => Output: event 'cancel' is illegal in state 'delivered'Run: python3 example.py
Output:
paid
shipped
delivered
event 'cancel' is illegal in state 'delivered'learning/code/ex-81-transition-table-order-lifecycle/test_example.py
"""Example 81: pytest verification that every illegal event is rejected by the table itself."""
import pytest
from example import IllegalTransition, OrderFsm
def test_happy_path_walks_created_paid_shipped_delivered() -> None:
order = OrderFsm()
assert order.send("pay") == "paid"
assert order.send("ship") == "shipped"
assert order.send("deliver") == "delivered"
def test_cancel_is_legal_from_created_and_paid_but_not_after() -> None:
order = OrderFsm()
order.send("pay")
assert order.send("cancel") == "cancelled" # => legal: paid -> cancelled
def test_shipped_order_cannot_be_cancelled_the_table_has_no_such_entry() -> None:
order = OrderFsm()
order.send("pay")
order.send("ship")
with pytest.raises(IllegalTransition, match="illegal in state 'shipped'"):
order.send("cancel") # => rejected by a MISSING key, not a special-cased if-check
def test_delivered_is_terminal_every_event_is_rejected() -> None:
order = OrderFsm()
order.send("pay")
order.send("ship")
order.send("deliver")
for event in ("pay", "ship", "deliver", "cancel"):
with pytest.raises(IllegalTransition):
order.send(event) # => no key exists for ("delivered", event) -- the table itself enforces this
# => Run: pytest -q -- Output: 4 passedVerify: pytest -q
Output:
4 passedKey takeaway: A transition table is executable documentation -- the set of legal moves is a data structure you can print and review, and "is this event legal here" is answered by dictionary membership, not by tracing through conditional branches.
Why it matters: Order lifecycles, approval workflows, and CI pipeline stages are all naturally table-shaped; encoding the table as data (rather than as scattered ifs across many methods) makes the whole state space auditable at a glance.
Example 82: Hierarchical Statechart -- Media Player
ex-82 · exercises co-36
A Harel statechart for a media player: playing is a PARENT state with two nested substates, normal and shuffle -- toggle_shuffle is handled at the CHILD level. stop is handled at the PARENT level, defined once, and applies no matter which substate is active. Volume is an ORTHOGONAL region, changing independently of the play state.
stateDiagram-v2
[*] --> stopped
stopped --> playing: play
state playing {
[*] --> normal
normal --> shuffle: toggle_shuffle
shuffle --> normal: toggle_shuffle
}
playing --> stopped: stop
learning/code/ex-82-statechart-hierarchical-media-player/example.py
"""Example 82: Hierarchical Statechart -- Media Player.
co-36: a Harel statechart for a media player. `Playing` is a PARENT state with
two nested substates, `Normal` and `Shuffle` -- `toggle_shuffle` is handled at
the CHILD level and only makes sense while inside `Playing`. `stop` is handled
at the PARENT level: it is defined ONCE on `Playing` and applies no matter
which substate is currently active (shared transition, not duplicated per
substate). Volume is an ORTHOGONAL region -- `volume_up`/`volume_down` change
independently of which play-state/substate is active.
"""
from __future__ import annotations # => defers type-hint evaluation for the dict[tuple[str, str], str] aliases below
class IllegalTransition(Exception): # => raised when neither the child nor parent table has an entry
pass # => a plain marker exception -- no extra fields needed, the message carries the detail
# ============================================================
# The nested (child-level) transitions -- only meaningful INSIDE the Playing parent state
# ============================================================
SUBSTATE_TRANSITIONS: dict[tuple[str, str], str] = { # => keys are (substate, event), checked FIRST in send()
("normal", "toggle_shuffle"): "shuffle", # => normal -> shuffle, on toggle_shuffle
("shuffle", "toggle_shuffle"): "normal", # => shuffle -> normal, on toggle_shuffle
} # => closes the CHILD-level table -- only reachable while state == "playing"
# ============================================================
# The parent-level (shared) transitions -- apply from ANY substate of Playing
# ============================================================
PARENT_TRANSITIONS: dict[tuple[str, str], str] = { # => keys are (state, event), checked SECOND in send()
("stopped", "play"): "playing", # => enters Playing; substate defaults to "normal"
("playing", "stop"): "stopped", # => co-36: ONE shared transition -- applies from EITHER substate
} # => closes the PARENT-level table -- shared across every substate of "playing"
class MediaPlayer: # => the composite state = (state, substate); volume is a fully independent region
def __init__(self) -> None: # => the constructor
self.state = "stopped" # => every player starts in this one top-level state
self.substate: str | None = None # => only meaningful while state == "playing"
self.volume = 50 # => the ORTHOGONAL region -- never touched by play-state transitions
def send(self, event: str) -> None: # => the ONE method that ever changes state/substate
# => 1. try the CHILD-level table first -- the more specific transition wins
if self.state == "playing" and self.substate is not None: # => only relevant while inside Playing
child_key = (self.substate, event) # => builds the (substate, event) lookup key
if child_key in SUBSTATE_TRANSITIONS: # => a CHILD-level match found
self.substate = SUBSTATE_TRANSITIONS[child_key] # => the child table supplies the next substate
return # => handled at the child level -- never falls through to the parent table
# => 2. bubble up to the PARENT-level table -- the shared transition, regardless of substate
parent_key = (self.state, event) # => builds the (state, event) lookup key
if parent_key in PARENT_TRANSITIONS: # => a PARENT-level match found
self.state = PARENT_TRANSITIONS[parent_key] # => the parent table supplies the next top-level state
self.substate = "normal" if self.state == "playing" else None # => entering Playing resets to "normal"
return # => handled at the parent level
# => 3. neither level has this event -- structurally illegal here
raise IllegalTransition(f"event {event!r} is illegal in state {self.state!r}/{self.substate!r}") # => honest failure
def adjust_volume(self, delta: int) -> None: # => the ORTHOGONAL region -- independent of state/substate
self.volume = max(0, min(100, self.volume + delta)) # => clamps to [0, 100], never touches state/substate
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
player = MediaPlayer() # => starts "stopped", substate None, per __init__
player.send("play") # => stopped -> playing (parent transition), substate defaults to normal
print(player.state, player.substate) # => confirms the parent transition fired
# => Output: playing normal
player.send("toggle_shuffle") # => nested-state event, handled at the CHILD level
print(player.substate) # => confirms the child-level substate transition fired
# => Output: shuffle
player.adjust_volume(20) # => the orthogonal region changes independently
print(player.volume) # => confirms volume moved, untouched by play-state/substate
# => Output: 70
player.send("stop") # => the PARENT's shared transition -- applies even though substate is "shuffle"
print(player.state, player.substate) # => confirms the ONE shared parent transition fired from ANY substate
# => Output: stopped None
print(player.volume) # => volume was never touched by the play-state transitions
# => Output: 70Run: python3 example.py
Output:
playing normal
shuffle
70
stopped None
70learning/code/ex-82-statechart-hierarchical-media-player/test_example.py
"""Example 82: pytest verification of nested-state events and the parent's shared transition."""
import pytest
from example import IllegalTransition, MediaPlayer
def test_play_enters_playing_with_normal_as_the_default_substate() -> None:
player = MediaPlayer()
player.send("play")
assert player.state == "playing"
assert player.substate == "normal"
def test_nested_state_event_toggles_the_child_substate() -> None:
player = MediaPlayer()
player.send("play")
player.send("toggle_shuffle") # => handled entirely at the CHILD level
assert player.substate == "shuffle"
player.send("toggle_shuffle")
assert player.substate == "normal"
def test_parents_shared_transition_applies_from_either_substate() -> None:
player = MediaPlayer()
player.send("play")
player.send("toggle_shuffle") # => now in the "shuffle" substate
assert player.substate == "shuffle"
player.send("stop") # => co-36: the PARENT's shared transition -- applies even from a non-default substate
assert player.state == "stopped"
assert player.substate is None
def test_volume_is_an_orthogonal_region_independent_of_play_state() -> None:
player = MediaPlayer()
player.adjust_volume(20) # => works even while stopped
assert player.volume == 70
player.send("play")
player.send("toggle_shuffle")
player.adjust_volume(-10) # => still works, regardless of substate
assert player.volume == 60
player.send("stop")
assert player.volume == 60 # => the play-state transition never touched the orthogonal volume region
def test_toggle_shuffle_is_illegal_while_stopped() -> None:
player = MediaPlayer()
with pytest.raises(IllegalTransition):
player.send("toggle_shuffle") # => the child transition only exists inside Playing
# => Run: pytest -q -- Output: 5 passedVerify: pytest -q
Output:
5 passedKey takeaway: Hierarchy lets a shared transition be written ONCE at the parent level instead of duplicated in every substate -- stop appears one time in PARENT_TRANSITIONS, not copy-pasted into both normal and shuffle, and the orthogonal volume region proves not every piece of state needs to live inside the same hierarchy at all.
Why it matters: Real UI and device state (media players, wizards, multi-step forms) is naturally hierarchical -- a flat transition table forces every shared behavior to be duplicated per leaf state, which is exactly the scaling problem Harel statecharts were invented to solve.
Example 83: Guards and Entry/Exit Actions -- Order FSM
ex-83 · exercises co-37
Extends the transition-table order FSM (ex-81) with a GUARD -- ship is only legal when the independent paid flag is true, checked on top of the table lookup -- plus ENTRY and EXIT actions that fire exactly once per state crossing.
learning/code/ex-83-guards-and-entry-exit-actions/example.py
"""Example 83: Guards and Entry/Exit Actions -- Order FSM.
co-37: extends the transition-table order FSM (ex-81) with a GUARD -- `ship`
is only legal when the independent `paid` flag is true, checked ON TOP OF the
table lookup, since the table alone (confirmed -> shipped) says nothing about
payment -- plus ENTRY and EXIT actions that fire exactly once per state
crossing: `log_entry` runs once on entering a state, `release_lock` runs once
on leaving one.
"""
from __future__ import annotations # => defers type-hint evaluation for the dataclass field annotations below
from dataclasses import dataclass, field # => field() gives each instance its own list, not a shared mutable default
# ============================================================
# Exceptions -- two DISTINCT failure kinds: structurally illegal vs. guard-blocked
# ============================================================
class IllegalTransition(Exception): # => the TABLE has no entry for (state, event)
pass # => a plain marker exception -- no extra fields needed, the message carries the detail
class GuardBlocked(Exception): # => the table allows it, but the GUARD condition is not met
pass # => a SEPARATE exception type from IllegalTransition -- a different failure kind, distinctly named
# => distinguishing the two exception types lets a caller react differently: retry after payment vs. a hard bug
# => co-37: the table alone cannot know about payment -- that is what the guard in send() checks separately
ORDER_TRANSITIONS: dict[tuple[str, str], str] = { # => keys are (state, event), values are the next state
("created", "confirm"): "confirmed", # => created -> confirmed, no guard needed here
("confirmed", "ship"): "shipped", # => the table says nothing about payment -- that is the guard's job
("shipped", "deliver"): "delivered", # => shipped -> delivered, no guard needed here
} # => closes the table -- structurally legal moves ONLY; payment is checked separately, in send()
@dataclass # => generates __init__ from the field declarations below
class GuardedOrderFsm: # => co-37: adds a guard clause plus entry/exit actions around the table lookup
# => lock_held is separate from state/paid -- it demonstrates that exit actions can affect ANY field, not just logs
state: str = "created" # => every order starts in this one state
paid: bool = False # => an INDEPENDENT flag, set by mark_paid(), not by any state transition
entry_log: list[str] = field(default_factory=list) # => records every ENTRY action firing
exit_log: list[str] = field(default_factory=list) # => records every EXIT action firing
lock_held: bool = True # => simulates a resource acquired on entry, released on exit
# => mark_paid() is the ONLY way the guard's condition ever changes -- send() only reads it
def mark_paid(self) -> None: # => flips the guard's condition, independent of the state machine
self.paid = True # => the ONE side effect -- send() below reads this flag, never sets it itself
# => send() runs the guard/entry/exit pipeline described in the module docstring, in order
def send(self, event: str) -> str: # => the ONE method that ever changes state
key = (self.state, event) # => builds the (state, event) lookup key for the table
if key not in ORDER_TRANSITIONS: # => step 1: the TABLE rejects structurally illegal events
raise IllegalTransition(f"event {event!r} is illegal in state {self.state!r}") # => an honest, specific failure
if event == "ship" and not self.paid: # => co-37: the GUARD -- checked IN ADDITION to the table entry
raise GuardBlocked("cannot ship: order is not paid") # => the table alone said this move was legal
old_state = self.state # => remembers the state being LEFT, for the exit action below
new_state = ORDER_TRANSITIONS[key] # => the table supplies the state being ENTERED
self._exit_action(old_state) # => co-37: EXIT action -- fires exactly once, leaving old_state
self.state = new_state # => the ONE line that actually crosses state boundaries
self._entry_action(new_state) # => co-37: ENTRY action -- fires exactly once, entering new_state
return self.state # => hands back the new state to the caller
# => entry/exit actions below are PRIVATE -- callers only ever interact through send()
def _entry_action(self, state: str) -> None: # => log-on-enter
self.entry_log.append(state) # => records exactly once per state crossing, never more
def _exit_action(self, state: str) -> None: # => release-lock-on-exit
self.exit_log.append(state) # => records exactly once per state crossing, never more
self.lock_held = False # => simulates releasing a resource held during the previous state
# => in a real system, this is where a database row lock or file handle would actually be released
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
# => the SAME order instance flows through every step below -- one FSM, five successive send()/log-check calls
fsm = GuardedOrderFsm() # => starts "created", unpaid, per the dataclass defaults
fsm.send("confirm") # => created -> confirmed: exit created, entry confirmed
print(fsm.entry_log, fsm.exit_log) # => confirms BOTH the entry and exit actions fired exactly once
# => Output: ['confirmed'] ['created']
try: # => attempts a move the table allows but the guard should block
fsm.send("ship") # => the TABLE allows confirmed -> shipped, but the GUARD blocks it: not yet paid
except GuardBlocked as error: # => the guard, not the table, produced this failure
print(error) # => shows the honest, specific failure message
# => Output: cannot ship: order is not paid
fsm.mark_paid() # => flips the independent guard condition
fsm.send("ship") # => now the guard passes: exit confirmed, entry shipped
print(fsm.state, fsm.entry_log, fsm.exit_log) # => confirms the guard's effect and both action logs updated
# => Output: shipped ['confirmed', 'shipped'] ['created', 'confirmed']Run: python3 example.py
Output:
['confirmed'] ['created']
cannot ship: order is not paid
shipped ['confirmed', 'shipped'] ['created', 'confirmed']learning/code/ex-83-guards-and-entry-exit-actions/test_example.py
"""Example 83: pytest verification that the guard blocks ship() and each action fires exactly once."""
import pytest
from example import GuardBlocked, GuardedOrderFsm, IllegalTransition
def test_guard_blocks_ship_when_the_order_is_not_yet_paid() -> None:
fsm = GuardedOrderFsm()
fsm.send("confirm")
with pytest.raises(GuardBlocked, match="not paid"):
fsm.send("ship") # => the table allows this transition; the GUARD is what blocks it
assert fsm.state == "confirmed" # => the state never advanced -- the guard fired before any mutation
def test_guard_passes_once_mark_paid_flips_the_independent_flag() -> None:
fsm = GuardedOrderFsm()
fsm.send("confirm")
fsm.mark_paid()
assert fsm.send("ship") == "shipped"
def test_entry_and_exit_actions_fire_exactly_once_per_state_crossing() -> None:
fsm = GuardedOrderFsm()
fsm.send("confirm")
fsm.mark_paid()
fsm.send("ship")
fsm.send("deliver")
assert fsm.entry_log == ["confirmed", "shipped", "delivered"] # => exactly one entry per state entered
assert fsm.exit_log == ["created", "confirmed", "shipped"] # => exactly one exit per state left
def test_lock_released_flag_flips_on_the_first_exit_action() -> None:
fsm = GuardedOrderFsm()
assert fsm.lock_held is True # => held from construction, before any transition
fsm.send("confirm")
assert fsm.lock_held is False # => released by the exit action leaving "created"
def test_illegal_event_never_fires_a_guard_or_an_action() -> None:
fsm = GuardedOrderFsm()
with pytest.raises(IllegalTransition):
fsm.send("deliver") # => no table entry for ("created", "deliver") at all
assert fsm.entry_log == [] # => no action fired for a transition the table itself rejected
assert fsm.exit_log == []
# => Run: pytest -q -- Output: 5 passedVerify: pytest -q
Output:
5 passedKey takeaway: A guard and a table entry answer different questions -- "does this transition exist at all" (the table) versus "is it allowed right now, given other state" (the guard) -- and keeping them separate means the table stays a clean map of structure while guards carry the business rules.
Why it matters: Almost every real workflow FSM needs at least one guard ("can only approve if the requester is not the approver," "can only refund within 30 days") -- modeling guards as a distinct, testable concern from the transition table keeps both simple.
Example 84: Transition-Table FSM vs. Boolean-Flag Soup -- a Direct Contrast
ex-84 · exercises co-35, co-33
Places the transition-table FSM (ex-81) side by side with the boolean-flag order (ex-60's FlagOrder, reproduced for a self-contained comparison). The flag version can represent is_shipped=True, is_paid=False -- a combination the business rules forbid. The FSM version cannot represent this at all: there is no path through the table that reaches "shipped" without first passing through "paid".
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph LR
subgraph FlagOrder["Boolean flags: 8 representable combinations"]
F1["is_paid=False, is_shipped=True<br/>(illegal, yet representable)"]:::brown
end
subgraph FSM["Transition-table FSM: 5 reachable states"]
S1["created"]:::blue --> S2["paid"]:::orange
S2 --> S3["shipped"]:::teal
S3 --> S4["delivered"]:::purple
end
classDef blue fill:#0173B2,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef orange fill:#DE8F05,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef teal fill:#029E73,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef purple fill:#CC78BC,stroke:#000000,color:#FFFFFF,stroke-width:2px
classDef brown fill:#CA9161,stroke:#000000,color:#FFFFFF,stroke-width:2px
learning/code/ex-84-fsm-vs-boolean-flags-contrast/example.py
"""Example 84: Transition-Table FSM vs. Boolean-Flag Soup -- a Direct Contrast.
co-35, co-33: places the transition-table FSM (ex-81) side by side with the
boolean-flag order (ex-60's `FlagOrder`, reproduced here for a self-contained
comparison). The flag version can represent `is_shipped=True, is_paid=False`
-- a combination the business rules say should be impossible. The FSM version
CANNOT represent this: "shipped" is a single string value, and there is no
combination of table entries that reaches it without first passing through
"paid". The illegal combination is unrepresentable, not just unreached.
"""
from __future__ import annotations # => defers type-hint evaluation for the dict[tuple[str, str], str] alias below
class IllegalTransition(Exception): # => raised when the FSM table has no entry for (state, event)
pass # => a plain marker exception -- no extra fields needed, the message carries the detail
# ============================================================
# BEFORE: boolean-flag soup -- co-33's starting point, reproduced for comparison
# ============================================================
class FlagOrder: # => three independent booleans -- 2**3 = 8 representable combinations
def __init__(self) -> None: # => the constructor
self.is_paid = False # => independent flag 1 of 3 -- nothing ties this to the other two
self.is_shipped = False # => independent flag 2 of 3 -- settable regardless of is_paid
self.is_cancelled = False # => independent flag 3 of 3 -- settable regardless of the other two
# ============================================================
# AFTER: the transition-table FSM (ex-81) -- one string field, only reachable states exist
# ============================================================
# => co-35: contrast with FlagOrder above -- ONE field (state), not three independent booleans
ORDER_TRANSITIONS: dict[tuple[str, str], str] = { # => keys are (state, event), values are the next state
("created", "pay"): "paid", # => created -> paid, on the "pay" event
("created", "cancel"): "cancelled", # => created -> cancelled, on the "cancel" event
("paid", "ship"): "shipped", # => paid -> shipped -- only reachable AFTER "paid"
("paid", "cancel"): "cancelled", # => paid -> cancelled, on the "cancel" event
("shipped", "deliver"): "delivered", # => shipped -> delivered, on the "deliver" event
} # => closes the table -- "shipped" only ever appears as a value reached THROUGH "paid", never around it
class OrderFsm: # => a single string field -- only states REACHABLE by walking the table can ever exist
def __init__(self) -> None: # => the constructor
self.state = "created" # => the ONE field this class has -- no independent booleans to fall out of sync
def send(self, event: str) -> str: # => the ONE method that ever changes self.state
key = (self.state, event) # => builds the (state, event) lookup key for the table
if key not in ORDER_TRANSITIONS: # => the TABLE rejects illegal events -- no scattered if-checks anywhere
raise IllegalTransition(f"event {event!r} is illegal in state {self.state!r}") # => an honest, specific failure
self.state = ORDER_TRANSITIONS[key] # => the table itself supplies the next state, no branching logic here
return self.state # => hands back the new state to the caller
def count_representable_flag_combinations() -> int: # => 3 booleans-worth of independence -> 2**3 combos, ALL settable
return 2 * 2 * 2 # => is_paid x is_shipped x is_cancelled, each independently True/False
# => co-33: the gap between these two counts IS the illegal-state surface that boolean flags leave open
def count_reachable_fsm_states() -> int: # => only the states that appear as a VALUE in the transition table
reachable = {"created"} | set(ORDER_TRANSITIONS.values()) # => created is the start; the rest come from the table
return len(reachable) # => a strictly smaller number than the flag version's combinations, by construction
if __name__ == "__main__": # => demonstration entry point, executed only when this file is run directly
flags = FlagOrder() # => starts with all three flags False
flags.is_shipped = True # => nothing in the FlagOrder class prevents setting this WITHOUT is_paid
print(flags.is_shipped, flags.is_paid) # => the illegal combination IS representable -- it just happened
# => Output: True False
fsm = OrderFsm() # => starts in "created", the FSM's single field
try: # => attempts the FSM equivalent of the same illegal combination above
fsm.send("ship") # => the FSM equivalent of "become shipped without paying first"
except IllegalTransition as error: # => the table's own omission produced this, not a manual if-check
print(error) # => structurally rejected: there is no table entry for ("created", "ship")
# => Output: event 'ship' is illegal in state 'created'
print(count_representable_flag_combinations()) # => 8 combinations the type system allows
# => Output: 8
print(count_reachable_fsm_states()) # => only 5 states are reachable at all -- illegal combos don't exist as values
# => Output: 5Run: python3 example.py
Output:
True False
event 'ship' is illegal in state 'created'
8
5learning/code/ex-84-fsm-vs-boolean-flags-contrast/test_example.py
"""Example 84: pytest verification that the FSM makes an illegal combination unrepresentable, unlike flags."""
import pytest
from example import (
FlagOrder,
IllegalTransition,
OrderFsm,
count_reachable_fsm_states,
count_representable_flag_combinations,
)
def test_flag_order_can_represent_the_illegal_shipped_without_paid_combination() -> None:
flags = FlagOrder()
flags.is_shipped = True # => nothing stops this -- the type itself allows the illegal combination
assert flags.is_shipped is True
assert flags.is_paid is False # => the combination the business rules forbid IS representable in this type
def test_fsm_cannot_reach_shipped_without_first_passing_through_paid() -> None:
fsm = OrderFsm()
with pytest.raises(IllegalTransition, match="illegal in state 'created'"):
fsm.send("ship") # => structurally rejected -- there is no path to "shipped" that skips "paid"
assert fsm.state == "created" # => state never advanced
def test_fsm_reaches_shipped_only_via_the_paid_state() -> None:
fsm = OrderFsm()
fsm.send("pay")
fsm.send("ship")
assert fsm.state == "shipped" # => the ONLY path to "shipped" passes through "paid"
def test_flag_soup_has_more_representable_combinations_than_the_fsm_has_reachable_states() -> None:
assert count_representable_flag_combinations() == 8 # => every boolean combination is a distinct value
assert count_reachable_fsm_states() == 5 # => strictly fewer -- illegal combinations simply do not exist
# => Run: pytest -q -- Output: 4 passedVerify: pytest -q
Output:
4 passedKey takeaway: "Unrepresentable" is a stronger guarantee than "unreached" -- a boolean-flag order CAN hold an illegal combination (nothing in the type stops it, it just has not happened yet in this run), while the FSM's single-state-value design makes the illegal combination impossible to construct in the first place.
Why it matters: This is the practical payoff of state-machine modeling: bugs caught by "the type does not allow this" are caught at every call site, forever, while bugs caught by "we haven't hit this path yet" depend on every future caller remembering the same discipline -- illegal-states-unrepresentable is a design property, not a habit.
← Previous: Intermediate Examples · Next: Capstone →
Last updated July 16, 2026