Module 9
OOP & design patterns
Design questions probe whether you can organize code so change stays cheap. The foundation is smaller than the acronyms suggest: encapsulation(hide state behind behavior, so invariants can't be violated from outside), polymorphism(callers hold an interface and don't care which implementation answers), and the modern default of composition over inheritance — build objects out of collaborators rather than deep class hierarchies, because a has-a relationship can be swapped and tested, while a base class is forever.
Design patternsare named, pre-argued solutions to recurring shapes of change. The value in a conversation isn't reciting structure — it's matching the scenario to the pattern and naming the trade-off. Six cover the overwhelming majority of questions:
| Pattern | The scenario that calls for it |
|---|---|
| Strategy | interchangeable algorithms behind one interface, chosen at runtime — pricing rules, sort orders, retry policies |
| Factory | creation is complex or conditional; callers ask for the product, not the constructor |
| Observer | many listeners react to one subject's changes without the subject knowing them — events, pub/sub, UI binding |
| Decorator | add behavior (logging, caching, retries) around an object without touching its class — wrap and delegate |
| Adapter | two incompatible interfaces must talk — a translating wrapper around the one you don't control |
| Singleton | exactly one instance process-wide — and the honest caveat: global state hurts tests; injecting one shared instance is usually cleaner |
🎛 Scenario → pattern quiz
Scenario 1 of 6
0/0 correct
The app must support several interchangeable pricing algorithms, chosen at runtime by name.
SOLID, one breath each
- S — Single responsibility: a class has one reason to change; parsing and persistence don't share a home
- O — Open/closed: add new behavior by adding code (a new strategy), not by editing a working switch statement
- L — Liskov substitution: a subclass must be usable anywhere its parent is — no surprise exceptions, no tightened preconditions
- I — Interface segregation: several small interfaces beat one fat one nobody fully implements
- D — Dependency inversion: depend on abstractions; pass collaborators in (constructor injection) instead of constructing them inside
Worked example — strategy in twelve lines
class FlatFee:
def cost(self, order): return 5.0
class PerItem:
def cost(self, order): return 0.6 * len(order.items)
class Checkout:
def __init__(self, shipping): # injected — Checkout never changes
self.shipping = shipping
def total(self, order):
return order.subtotal + self.shipping.cost(order)
# New shipping rule later? Add a class, register it. Checkout untouched —
# that's the open/closed principle with a name and an address.Say it out loud
- “The requirement is ‘new algorithms will be added’ — that's strategy, so additions don't modify tested code.”
- “I'd compose rather than subclass here — I can inject a fake in tests and swap implementations later.”
- “A singleton works, but it's global state — I'd rather construct one instance at startup and inject it.”