The book opens with Joe and his SimUDuck app. The problem: not all ducks fly or quack the same way. By placing fly() in the base Duck class, rubber ducks and decoy ducks inherit behavior that doesn't belong to them. This is the classic inheritance problem -- modifying the base class affects all subclasses.
Define a family of algorithms, encapsulate each one, and make them interchangeable at runtime.
Before Strategy, Joe used inheritance: class Duck had fly() and quack(). But a DecoyDuck shouldn't quack, and a RubberDuck can't fly. Every new duck behavior required a new subclass, and the inheritance tree exploded. Strategy solves this by extracting the varying behavior into its own class hierarchy and composing it into Duck.
- Context (
Duck): the class that holds a reference to the strategy - Strategy (
FlyBehavior,QuackBehavior): the interface/abstract class all concrete strategies implement - Concrete Strategy (
FlyWithWings,FlyNoWay,Quack,Squeak): each one is a specific algorithm
from abc import ABC, abstractmethod
# ---- Strategy Interfaces ----
class FlyBehavior(ABC):
"""Strategy interface for all flight algorithms."""
@abstractmethod
def fly(self) -> None:
pass
class QuackBehavior(ABC):
"""Strategy interface for all quack algorithms."""
@abstractmethod
def quack(self) -> None:
pass
# ---- Concrete Strategies: Flight ----
class FlyWithWings(FlyBehavior):
def fly(self) -> None:
print("I'm flying with wings!")
class FlyNoWay(FlyBehavior):
def fly(self) -> None:
print("I can't fly")
class FlyRocketPowered(FlyBehavior):
def fly(self) -> None:
print("I'm flying with a rocket!")
# ---- Concrete Strategies: Quacking ----
class Quack(QuackBehavior):
def quack(self) -> None:
print("Quack")
class MuteQuack(QuackBehavior):
def quack(self) -> None:
print("<< silence >>")
class Squeak(QuackBehavior):
def quack(self) -> None:
print("Squeak")Instead of inheriting behavior, Duck has instances of FlyBehavior and QuackBehavior. Notice the setter methods -- they allow changing behavior at runtime.
class Duck(ABC):
"""Context: composes FlyBehavior and QuackBehavior instead of inheriting them."""
def __init__(
self,
fly_behavior: FlyBehavior | None = None,
quack_behavior: QuackBehavior | None = None,
):
self._fly_behavior = fly_behavior
self._quack_behavior = quack_behavior
# Dynamic behavior replacement -- the power of Strategy
def set_fly_behavior(self, behavior: FlyBehavior) -> None:
self._fly_behavior = behavior
def set_quack_behavior(self, behavior: QuackBehavior) -> None:
self._quack_behavior = behavior
def perform_fly(self) -> None:
if self._fly_behavior:
self._fly_behavior.fly()
def perform_quack(self) -> None:
if self._quack_behavior:
self._quack_behavior.quack()
def swim(self) -> None:
"""All ducks float -- common behavior stays in the base class."""
print("All ducks float, even decoys!")
@abstractmethod
def display(self) -> None:
passEach duck is configured with its own combination of strategies in __init__:
class MallardDuck(Duck):
def __init__(self):
super().__init__(FlyWithWings(), Quack())
def display(self) -> None:
print("I'm a real Mallard duck")
class RubberDuck(Duck):
def __init__(self):
super().__init__(FlyNoWay(), Squeak())
def display(self) -> None:
print("I'm a rubber duckie")
class DecoyDuck(Duck):
def __init__(self):
super().__init__(FlyNoWay(), MuteQuack())
def display(self) -> None:
print("I'm a decoy duck")This is the key moment that shows why Strategy wins over inheritance:
# Each duck has the right behavior baked in at construction
mallard = MallardDuck()
mallard.perform_fly() # I'm flying with wings!
mallard.perform_quack() # Quack
# But we can swap the strategy at runtime
mallard.set_fly_behavior(FlyRocketPowered())
mallard.perform_fly() # I'm flying with a rocket!
# Decoy and rubber ducks never needed fly/quack overridden
decoy = DecoyDuck()
decoy.perform_fly() # I can't fly
decoy.perform_quack() # << silence >>FlyBehavior (Strategy - interface)
├── FlyWithWings (ConcreteStrategy)
├── FlyNoWay (ConcreteStrategy)
└── FlyRocketPowered (ConcreteStrategy)
QuackBehavior (Strategy - interface)
├── Quack (ConcreteStrategy)
├── MuteQuack (ConcreteStrategy)
└── Squeak (ConcreteStrategy)
Duck (Context - has-a FlyBehavior, has-a QuackBehavior)
├── MallardDuck
├── RubberDuck
└── DecoyDuck
- Define a strategy interface for each behavior that varies (
FlyBehavior,QuackBehavior) - Write concrete strategy classes for each algorithm variant
- In the context (
Duck), maintain a reference to the strategy interface (not a concrete class) - When the behavior is needed, delegate to the strategy:
self._fly_behavior.fly() - Optionally expose setters to change the strategy at runtime
- Identify what varies and encapsulate it: fly and quack behavior vary, so each gets its own class hierarchy
- Program to interfaces, not implementations:
DuckholdsFlyBehavior, notFlyWithWings - Favor composition over inheritance: a Duck has-a
FlyBehaviorinstead of being-aFlyableDuck - Open/Closed Principle: you can add new behaviors (
FlyRocketPowered) without modifying any existing class
ABC+abstractmethodfromabcreplaces Java'sinterfacekeyword for behavior contracts- The
| Nonetype hint (Python 3.10+) allows strategies to be optional - In Python, you could alternatively use first-class functions or lambdas as strategies (since functions are already objects), but using classes makes the pattern more explicit and testable
Observer defines a one-to-many dependency between objects so that when one object changes state, all its dependents are automatically notified and updated.
Think of it like subscribing to a newspaper:
- Publisher (Subject) → produces content periodically
- Subscribers (Observers) → receive a copy when a new edition arrives
- Subscribe =
register_observer(), Unsubscribe =remove_observer()
Before Observer, the weather station had a huge display() method that pushed new measurements to every display panel in a tight coupling. Every time you added a new display type, you had to modify WeatherData. Observer decouples the subject from its observers, so you can add or remove displays without touching the subject.
- Subject (aka Observable): the object being observed. Maintains a list of observers and can add/remove them
- Observer: receives automatic update notifications when the subject changes state
- Concrete Subject: the real object that holds data (here:
WeatherData) - Concrete Observer: receives data and updates its own internal state
The weather subject maintains temperature, humidity, and pressure. Three display panels act as observers: current conditions, statistics, and forecast.
The Subject and Observer interfaces decouple the two sides:
from abc import ABC, abstractmethod
# ---- Subject Interface ----
class Subject(ABC):
"""Any class that can be observed must support these operations."""
@abstractmethod
def register_observer(self, observer: "Observer") -> None:
"""Add an observer to the notification list."""
pass
@abstractmethod
def remove_observer(self, observer: "Observer") -> None:
"""Remove an observer from the notification list."""
pass
@abstractmethod
def notify_observers(self) -> None:
"""Alert all observers that state has changed."""
pass
# ---- Observer Interface ----
class Observer(ABC):
"""Any observer must know how to receive an update."""
@abstractmethod
def update(self, temp: float, humidity: float, pressure: float) -> None:
pass
# ---- DisplayComponent Interface ----
class DisplayComponent(ABC):
"""Any display panel knows how to render itself."""
@abstractmethod
def display(self) -> None:
passWeatherData both implements Subject and owns the weather data. When new hardware readings arrive, it calls measurements_changed() → notify_observers():
class WeatherData(Subject):
"""The weather station. Implements Subject to be observable."""
def __init__(self):
self._observers: list[Observer] = []
self._temperature: float = 0.0
self._humidity: float = 0.0
self._pressure: float = 0.0
# ---- Subject implementation ----
def register_observer(self, observer: Observer) -> None:
self._observers.append(observer)
def remove_observer(self, observer: Observer) -> None:
self._observers.remove(observer)
def notify_observers(self) -> None:
"""Push current state to every registered observer."""
for observer in self._observers:
observer.update(
self._temperature, self._humidity, self._pressure
)
# ---- Weather station domain logic ----
def measurements_changed(self) -> None:
"""Called when the hardware delivers new sensor readings."""
self.notify_observers()
def set_measurements(
self, temperature: float, humidity: float, pressure: float
) -> None:
"""Update internal state and notify all observers."""
self._temperature = temperature
self._humidity = humidity
self._pressure = pressure
self.measurements_changed()Each display registers itself to WeatherData in its constructor. When update() is called, it pulls the new data and re-renders:
class CurrentConditionsDisplay(Observer, DisplayComponent):
"""Shows the current temperature and humidity."""
def __init__(self, weather_data: WeatherData):
self._temperature: float = 0.0
self._humidity: float = 0.0
# Register to receive updates
weather_data.register_observer(self)
def update(self, temp: float, humidity: float, pressure: float) -> None:
self._temperature = temp
self._humidity = humidity
self.display()
def display(self) -> None:
print(
f"Current conditions: "
f"{self._temperature}°F, "
f"{self._humidity}% humidity"
)
class StatisticsDisplay(Observer, DisplayComponent):
"""Tracks running average, min, and max temperature."""
def __init__(self, weather_data: WeatherData):
self._avg_temp: float = 0.0
self._count: int = 0
weather_data.register_observer(self)
def update(self, temp: float, humidity: float, pressure: float) -> None:
self._count += 1
self._avg_temp = (self._avg_temp + temp) / self._count
self.display()
def display(self) -> None:
print(f"Statistics: avg temp = {self._avg_temp:.1f}°F")
class ForecastDisplay(Observer, DisplayComponent):
"""Predicts weather based on atmospheric pressure."""
def __init__(self, weather_data: WeatherData):
self._pressure: float = 0.0
weather_data.register_observer(self)
def update(self, temp: float, humidity: float, pressure: float) -> None:
self._pressure = pressure
self.display()
def display(self) -> None:
if self._pressure > 28.0:
print("Forecast: Sunny and warm!")
elif self._pressure > 26.0:
print("Forecast: Some clouds and rain")
else:
print("Forecast: Cool with rain!")Notice how the observers are created, registered automatically, and updated without WeatherData knowing their types:
weather = WeatherData()
current = CurrentConditionsDisplay(weather)
stats = StatisticsDisplay(weather)
forecast = ForecastDisplay(weather)
# Simulate new sensor readings - all 3 displays update automatically
weather.set_measurements(80.0, 65.0, 30.4)
# Current conditions: 80.0°F, 65.0% humidity
# Statistics: avg temp = 80.0°F
# Forecast: Sunny and warm!
weather.set_measurements(82.0, 70.0, 29.0)
# Current conditions: 82.0°F, 70.0% humidity
# Statistics: avg temp = 81.0°F
# Forecast: Some clouds and rain
# Unsubscribe a display
weather.remove_observer(forecast)
weather.set_measurements(78.0, 90.0, 25.0)
# Only current and stats update nowSubject (interface)
├── register_observer()
├── remove_observer()
└── notify_observers()
WeatherData (ConcreteSubject, implements Subject)
Observer (interface)
└── update(temp, humidity, pressure)
DisplayComponent (interface)
└── display()
CurrentConditionsDisplay (implements Observer + DisplayComponent)
StatisticsDisplay (implements Observer + DisplayComponent)
ForecastDisplay (implements Observer + DisplayComponent)
- Push model (used here): Subject pushes all its data to each observer through
update(). The observer receives everything and decides what to use. - Pull model: Subject only passes a reference to itself in
update(). The observer calls getter methods on the subject to pull only the data it needs.
Both are valid -- push is simpler, pull is more data-efficient.
- Loose coupling:
WeatherDataonly knows it has a list ofObserverinterfaces. It doesn't know (or care) what concrete classes they are - Open/Closed: You can add new displays (e.g.,
HeatIndexDisplay) without modifyingWeatherData - Broadcast communication: One
notify_observers()call reaches every subscriber - Runtime flexibility: Observers subscribe/unsubscribe dynamically
Python has built-in mechanisms that follow the same idea:
typing.Protocolfor duck-typed observers- Decorator-based signal handling (like Django signals, SQLAlchemy events)
- Asyncio event loops and callbacks
collections.abcobserver-like patterns
# Lightweight observer using plain callbacks
class SimpleObservable:
"""A minimal push-observer using Python first-class functions."""
def __init__(self):
self._callbacks: list[callable] = []
def register(self, callback: callable) -> None:
self._callbacks.append(callback)
def remove(self, callback: callable) -> None:
self._callbacks.remove(callback)
def notify(self, *args, **kwargs) -> None:
for callback in self._callbacks:
callback(*args, **kwargs)- Notification ordering: observers are notified in list order, but order isn't guaranteed -- don't depend on it
- Memory leaks: if observers only hold strong references to the subject but are never removed, they can't be garbage collected
- Exception propagation: one observer raising an exception may prevent others from receiving the update
Dynamically attach additional responsibilities to an object. Decorators provide a flexible alternative to subclassing for extending functionality.
Imagine a coffee shop ordering system. You have beverages: HouseBlend, DarkRoast, Espresso, Decaf. You also have condiments: Soy, Mocha, Whip.
With inheritance, you could create subclasses for every combination: HouseBlendWithSoy, HouseBlendWithMocha, HouseBlendWithSoyAndMocha, DarkRoastWithWhipAndMocha... The number of subclasses explodes exponentially. With 4 beverages and 3 condiments, you need 4 × 8 = 32 classes (for single/double/triple combo). This is unmaintainable.
- Component (
Beverage): the interface/abstract class that decorators and concrete components both implement - Concrete Component (
Espresso,DarkRoast): a base object without decorators - Decorator (
CondimentDecorator): an abstract class that wraps a component and adds behavior - Concrete Decorator (
Mocha,Soy,Whip): each adds its own additional cost/description
Both beverages AND condiments implement the same Beverage interface. This is the key to making decorators transparent: the client can't tell (and doesn't need to know) if a beverage is decorated or not.
from abc import ABC, abstractmethod
class Beverage(ABC):
"""Shared interface for all beverages and their decorators."""
@abstractmethod
def description(self) -> str:
"""Return what this beverage is."""
pass
@abstractmethod
def cost(self) -> float:
"""Return total cost (base price + condiment prices)."""
passThese are "undecorated" beverages with a base price and description:
class HouseBlend(Beverage):
def description(self) -> str:
return "House Blend Coffee"
def cost(self) -> float:
return 0.89
class DarkRoast(Beverage):
def description(self) -> str:
return "Dark Roast Coffee"
def cost(self) -> float:
return 0.99
class Espresso(Beverage):
def description(self) -> str:
return "Espresso"
def cost(self) -> float:
return 1.99
class Decaf(Beverage):
def description(self) -> str:
return "Decaf Coffee"
def cost(self) -> float:
return 1.05CondimentDecorator is itself a Beverage. Every decorator wraps a Beverage instance inside it. This is how decorators chain together:
class CondimentDecorator(Beverage):
"""Abstract decorator: wraps a Beverage and delegates to it."""
def __init__(self, beverage: Beverage):
self._beverage = beverageEach decorator overrides description() and cost() to add its own layer on top of the wrapped beverage:
class Mocha(CondimentDecorator):
def description(self) -> str:
# Delegate to wrapped beverage, then append own description
return f"{self._beverage.description()}, Mocha"
def cost(self) -> float:
# Add own cost to wrapped beverage's cost
return self._beverage.cost() + 0.20
class Soy(CondimentDecorator):
def description(self) -> str:
return f"{self._beverage.description()}, Soy"
def cost(self) -> float:
return self._beverage.cost() + 0.15
class Whip(CondimentDecorator):
def description(self) -> str:
return f"{self._beverage.description()}, Whip"
def cost(self) -> float:
return self._beverage.cost() + 0.10Each decorator wraps one object and adds its own behavior. The call propagates inward like onion layers:
Whip(Mocha(Mocha(Espresso())))
│ │ │ │
│ │ │ description() → "Espresso", cost() → 1.99
│ │ description() → "..., Mocha", cost() → 1.99 + 0.20 = 2.19
│ description() → "..., Mocha", cost() → 2.19 + 0.20 = 2.39
description() → "..., Whip", cost() → 2.39 + 0.10 = 2.49
You can build any combination without new classes:
# Double mocha espresso with whip
beverage: Beverage = Espresso() # Espresso: $1.99
beverage = Mocha(beverage) # + Mocha: $2.19
beverage = Mocha(beverage) # + Mocha: $2.39
beverage = Whip(beverage) # + Whip: $2.49
print(f"{beverage.description()} ${beverage.cost():.2f}")
# Espresso, Mocha, Mocha, Whip $2.49
# Soy house blend with mocha
beverage2: Beverage = HouseBlend() # House Blend: $0.89
beverage2 = Soy(beverage2) # + Soy: $1.04
beverage2 = Mocha(beverage2) # + Mocha: $1.24
print(f"{beverage2.description()} ${beverage2.cost():.2f}")
# House Blend Coffee, Soy, Mocha $1.24Beverage (Component - abstract)
├── description()
└── cost()
├── HouseBlend (ConcreteComponent)
├── DarkRoast (ConcreteComponent)
├── Espresso (ConcreteComponent)
└── Decaf (ConcreteComponent)
CondimentDecorator (Decorator - abstract, extends Beverage)
├── self._beverage: Beverage (the wrapped component)
├── Mocha (ConcreteDecorator)
├── Soy (ConcreteDecorator)
└── Whip (ConcreteDecorator)
- Create a concrete beverage:
Espresso()-- this is the innermost layer - Wrap it with decorators:
Whip(Mocha(Espresso()))-- each wrapper adds its own cost/description - When you call
description()orcost()on the outermost decorator, it delegates inward through each layer - You can stack decorators infinitely without creating new classes
- Open/Closed Principle: add new condiments (
Chocolate) without modifying any existing class - Composite Responsibility: each decorator "wraps" a component and adds behavior
- Favor composition over inheritance: instead of 32 subclasses, you have 4 + 3 = 7 classes that compose endlessly
- Classes should be open for extension, closed for modification
- Python's built-in
@decoratorsyntax is a different pattern entirely -- it's a compile-time transformation. The Decorator Pattern discussed here is about runtime object wrapping. - The
functools.wrapsdecorator is inspired by this pattern -- it preserves function metadata when wrapping. - In Python, decorators are extremely common in frameworks: Flask route decorators (
@app.route), Django permission decorators (@login_required), and type checking (@overload).
- Too many decorators: more than 4-5 nested decorators becomes hard to debug. Consider if composition of behaviors or a builder pattern would be cleaner.
- Decorator ordering matters:
Soy(Mocha(beverage))may give a different description thanMocha(Soy(beverage)) - Double decorator: wrapping the same decorator twice (
Mocha(Mocha(beverage))) works but should be intentional
Define an interface for creating an object, but let the subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses.
Joe's Pizza framework started with a simple PizzaStore that had orderPizza(). Inside, it used new CheesePizza() to create pizzas. But when the franchise expanded, each city wanted its own style: NY-style thin crust vs. Chicago deep dish. Hardcoding the pizza type breaks open/closed -- every new city requires modifying PizzaStore.
There are three distinct patterns that build on each other. Understanding the difference is crucial:
| Level | Name | Who creates? | GoF pattern? |
|---|---|---|---|
| 0 | Simple Factory | A function/static method makes the decision | No (not formal GoF) |
| 1 | Factory Method | Subclasses override the creation method | Yes |
| 2 | Abstract Factory | An object creates families of related products | Yes |
A simple factory is just a function or static method that decides which object to create. It's NOT one of the 23 GoF patterns, but it's a useful first step:
# Simple Factory: a function that creates pizzas
def create_pizza(pizza_type: str) -> "Pizza | None":
if pizza_type == "cheese":
return CheesePizza()
elif pizza_type == "clam":
return ClamPizza()
elif pizza_type == "pepperoni":
return PepperoniPizza()
return None
# Problem: every new pizza type or city style requires modifying this functionThe issue: the PizzaStore is still coupled directly to a concrete CheesePizza class. Adding a new city style means modifying PizzaStore, violating open/closed.
The solution: pull the new out of orderPizza() into its own method, createPizza(). Subclasses override it:
from abc import ABC, abstractmethod
class Pizza(ABC):
"""Base class for all pizza types."""
def __init__(self):
self.name: str = ""
self.dough: str = ""
self.sauce: str = ""
def prepare(self) -> None:
print(f"Preparing {self.name}")
def bake(self) -> None:
print("Bake for 25 minutes at 350°F")
def cut(self) -> None:
print("Cutting the pizza into diagonal slices")
def box(self) -> None:
print("Place pizza in official boxing")
@abstractmethod
def __str__(self) -> str:
pass
class PizzaStore(ABC):
"""Defines the skeleton of orderPizza(). Subclasses create the pizzas."""
@abstractmethod
def create_pizza(self, pizza_type: str) -> Pizza:
"""Factory Method: each subclass overrides this to return its own pizza."""
pass
def order_pizza(self, pizza_type: str) -> Pizza:
"""Template method -- note how it depends only on the Pizza abstraction."""
pizza = self.create_pizza(pizza_type) # Defer creation to subclass
pizza.prepare()
pizza.bake()
pizza.cut()
pizza.box()
return pizza
class NYPizzaStore(PizzaStore):
"""NY branch creates NY-style pizzas."""
def create_pizza(self, pizza_type: str) -> Pizza:
if pizza_type == "cheese":
return NYStyleCheesePizza()
elif pizza_type == "clam":
return NYStyleClamPizza()
elif pizza_type == "pepperoni":
return NYStylePepperoniPizza()
raise ValueError(f"Unknown pizza type: {pizza_type}")
class ChicagoPizzaStore(PizzaStore):
"""Chicago branch creates Chicago-style pizzas."""
def create_pizza(self, pizza_type: str) -> Pizza:
if pizza_type == "cheese":
return ChicagoStyleCheesePizza()
elif pizza_type == "clam":
return ChicagoStyleClamPizza()
elif pizza_type == "pepperoni":
return ChicagoStylePepperoniPizza()
raise ValueError(f"Unknown pizza type: {pizza_type}")
# Concrete pizza classes
class NYStyleCheesePizza(Pizza):
def __init__(self):
super().__init__()
self.name = "NY Style Sauce and Cheese Pizza"
def __str__(self) -> str:
return self.name
class ChicagoStyleCheesePizza(Pizza):
def __init__(self):
super().__init__()
self.name = "Chicago Style Deep Dish Cheese Pizza"
def __str__(self) -> str:
return self.name# Same order_pizza() code, but each store creates its own pizza variety
ny_store = NYPizzaStore()
chicago_store = ChicagoPizzaStore()
ny_pizza = ny_store.order_pizza("cheese")
# Preparing NY Style Sauce and Cheese Pizza
# Bake for 25 minutes at 350°F
# Cutting the pizza into diagonal slices
# Place pizza in official boxing
chicago_pizza = chicago_store.order_pizza("cheese")
# Preparing Chicago Style Deep Dish Cheese Pizza
# Bake for 25 minutes at 350°F
# Cutting the pizza into diagonal slices
# Place pizza in official boxingPizza (AbstractProduct)
├── NYStyleCheesePizza (ConcreteProduct)
├── NYStyleClamPizza (ConcreteProduct)
├── ChicagoStyleCheesePizza (ConcreteProduct)
└── ChicagoStyleClamPizza (ConcreteProduct)
PizzaStore (Creator - abstract)
├── create_pizza(pizza_type) → abstract (Factory Method)
└── order_pizza(type) → concrete template
├── NYPizzaStore (ConcreteCreator)
│ └── create_pizza() → NY-style pizzas
└── ChicagoPizzaStore (ConcreteCreator)
└── create_pizza() → Chicago-style pizzas
Now each city also has different ingredients. NY uses Reggiano cheese, Chicago uses Mozzarella. Abstract Factory creates families of related products:
class PizzaIngredientFactory(ABC):
"""Abstract Factory: creates a family of related ingredients."""
@abstractmethod
def create_dough(self) -> str:
pass
@abstractmethod
def create_sauce(self) -> str:
pass
@abstractmethod
def create_cheese(self) -> str:
pass
class NYPizzaIngredientFactory(PizzaIngredientFactory):
"""NY ingredient family."""
def create_dough(self) -> str:
return "Thin Crust Dough"
def create_sauce(self) -> str:
return "Marinara Sauce"
def create_cheese(self) -> str:
return "Shredded Reggiano Cheese"
class ChicagoPizzaIngredientFactory(PizzaIngredientFactory):
"""Chicago ingredient family."""
def create_dough(self) -> str:
return "Thick Crust Dough"
def create_sauce(self) -> str:
return "Chicago Style Tomato Sauce"
def create_cheese(self) -> str:
return "Shredded Mozzarella Cheese"
class CheesePizza(Pizza):
def __init__(self, ingredient_factory: PizzaIngredientFactory):
super().__init__()
self._ingredient_factory = ingredient_factory
self.name = "Cheese Pizza"
def prepare(self) -> None:
print(f"Preparing {self.name}")
self.dough = self._ingredient_factory.create_dough()
self.sauce = self._ingredient_factory.create_sauce()
self.cheese = self._ingredient_factory.create_cheese()
# PizzaStore injects the right ingredient factory
class NYPizzaStore(PizzaStore):
def create_pizza(self, pizza_type: str) -> Pizza:
factory = NYPizzaIngredientFactory() # NY family
if pizza_type == "cheese":
return CheesePizza(factory)
elif pizza_type == "clam":
return ClamPizza(factory)
raise ValueError(f"Unknown: {pizza_type}")
class ChicagoPizzaStore(PizzaStore):
def create_pizza(self, pizza_type: str) -> Pizza:
factory = ChicagoPizzaIngredientFactory() # Chicago family
if pizza_type == "cheese":
return CheesePizza(factory)
elif pizza_type == "clam":
return ClamPizza(factory)
raise ValueError(f"Unknown: {pizza_type}")
# Testing Abstract Factory + Factory Method combined
ny_store = NYPizzaStore()
pizza = ny_store.order_pizza("cheese")
# Preparing Cheese Pizza (with NY ingredients: Thin Crust, Marinara, Reggiano)PizzaIngredientFactory (Abstract Factory)
├── create_dough()
├── create_sauce()
└── create_cheese()
├── NYPizzaIngredientFactory (Concrete Factory)
│ → Thin Crust, Marinara, Reggiano
└── ChicagoPizzaIngredientFactory (Concrete Factory)
→ Thick Crust, Chicago Tomato, Mozzarella
CheesePizza receivesingredient_factory and calls its methods
- Factory Method: uses inheritance. Subclasses override a single method to decide the product. One product per factory method call.
- Abstract Factory: uses composition. The factory object has multiple creation methods that return related products (a "product family"). Multiple related products created together.
Factory Method = one thing. Abstract Factory = a family of things that go together.
- High-level modules should NOT depend on low-level modules. Both should depend on abstractions.
PizzaStore(high-level) depends onPizza(abstraction), not onNYCheesePizza(low-level)- Details ("NYCheesePizza") depend on abstractions ("Pizza"), not the other way around
- Simple Factory: a function that creates objects -- useful but not extensible
- Factory Method: subclass overrides a method to control object creation -- decouples the creator from the product
- Abstract Factory: factory object creates families of related objects -- ensures products work together correctly
- Python doesn't have checked exceptions, so
create_pizza()returning a wrong type will only show at runtime. Consider returningNoneor raisingValueError. factory patternin Python can use plain functions as factories (they're first-class objects). The formal pattern is still useful when you need subclassing and type contracts.typing.Protocolcan serve as a factory interface without requiring explicit inheritance.
Ensure a class has only one instance, and provide a global point of access to it.
Some resources should only exist once in your program: a database connection pool, a logger, hardware device driver, or application configuration. Creating multiple instances would waste resources or cause inconsistent state. Singleton guarantees exactly one instance.
- Connection pools: one pool for all database/network connections
- Caches: one shared in-memory cache for the application
- Configuration / settings: one central configuration object
- Logging: one logger instance that all modules share
- Hardware devices: one driver for a single physical device (printer, graphics card)
class Singleton:
"""Override __new__ to always return the same instance."""
_instance: "Singleton | None" = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
# Test
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # True -- same objectHow it works: __new__ is called before __init__. By controlling __new__, we return the cached instance instead of creating a new one.
A Python metaclass is the "class of a class" -- it controls how classes are created. A singleton metaclass can enforce singleton behavior on any class:
class SingletonMeta(type):
"""Metaclass that enforces singleton behavior on any class using it."""
_instances: dict[type, object] = {}
def __call__(cls, *args, **kwargs):
if cls not in cls._instances:
# First creation: call the normal __call__ chain
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
else:
# Subsequent calls: return cached instance, ignoring new args
instance = cls._instances[cls]
return instance
class Logger(metaclass=SingletonMeta):
"""Any class using this metaclass becomes a singleton."""
def log(self, message: str) -> None:
print(f"[LOG] {message}")
class Database(metaclass=SingletonMeta):
"""Another singleton, completely independent from Logger."""
def __init__(self):
self._connection = "DB connection initialized"
def query(self, sql: str) -> list:
return [f"Result of {sql}"]
# Test: Logger and Database are each singletons, but independent
log1 = Logger()
log2 = Logger()
print(log1 is log2) # True -- single Logger instance
db1 = Database()
db2 = Database()
print(db1 is db2) # True -- single Database instance
print(log1 is db1) # False -- different classes, different instancesIn Python, a module is only loaded and executed once per process. This makes every module a natural singleton:
# config.py - This IS a singleton because the module loads once
DATABASE_URL = "sqlite:///app.db"
DEBUG = False
MAX_CONNECTIONS = 10
# Anywhere else in the application:
# from config import DATABASE_URL -- always the same objectThis is the most Pythonic Singleton. No special code needed -- the import system guarantees one instance.
The basic __new__ approach is NOT thread-safe. Under heavy concurrency, two threads might create two instances before either finishes. Fix with a lock:
import threading
class ThreadSafeSingleton:
_instance: "ThreadSafeSingleton | None" = None
_lock = threading.Lock()
def __new__(cls):
# Double-checked locking: fast path first, lock only when needed
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance- Hidden dependencies: any code can access the singleton without declaring the dependency -- hard to trace where it's used
- Testing difficulty: singletons maintain state across tests. You need to manually reset or mock them
- Extensibility: you can't easily subclass a singleton pattern -- the class is hardcoded to be unique
- Tight coupling: calling
Singleton.getInstance()couples you to a specific implementation
Guideline: Use Singleton only when you truly need a single global instance. Prefer dependency injection or module-level objects in Python.
- Python's GIL (Global Interpreter Lock) simplifies single-threaded singleton usage
threading.Lockis needed for true multi-threaded safety- Module-level
importis the idiomatic Python approach for shared state contextvarsprovides per-context (async) singletons for concurrent apps
Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
Joe's remote control app lets users press a button and something happens. But what happens depends on which device is plugged into which slot. Without Command, the remote would need to know about every device type, every device method, and how to call them. This tight coupling means every new device requires modifying the remote.
- Command (interface/abstract): declares the interface all concrete commands implement (
execute()) - Concrete Command: binds a Receiver with a specific action. Stores a reference to the receiver and calls its method in
execute() - Invoker (the remote): holds commands and triggers them. The invoker doesn't know about the receiver
- Receiver (the light): knows how to carry out the operation
- Client: creates the concrete command and sets its receiver
All commands share this one method. This is the contract the remote talks to:
from abc import ABC, abstractmethod
class Command(ABC):
@abstractmethod
def execute(self) -> None:
pass
@abstractmethod
def undo(self) -> None:
"""Support undo for the last command executed."""
passEach command pairs a receiver with an action. Notice how LightOnCommand knows about the Light:
class Light:
"""The receiver -- knows how to turn on and off."""
def __init__(self, name: str = "Living Room"):
self._name = name
def on(self) -> None:
print(f"{self._name} light is ON")
def off(self) -> None:
print(f"{self._name} light is OFF")
class LightOnCommand(Command):
"""Binds a Light receiver with the 'on' action."""
def __init__(self, light: Light):
self._light = light
def execute(self) -> None:
self._light.on()
def undo(self) -> None:
self._light.off()
class LightOffCommand(Command):
"""Binds a Light receiver with the 'off' action."""
def __init__(self, light: Light):
self._light = light
def execute(self) -> None:
self._light.off()
def undo(self) -> None:
self._light.on()The remote has slots. Each slot holds an ON command and an OFF command. The remote only talks through the Command interface:
class RemoteControl:
"""Invoker: presses buttons and stores undo history."""
def __init__(self, num_slots: int = 7):
self._on_commands: list[Command | None] = [None] * num_slots
self._off_commands: list[Command | None] = [None] * num_slots
self._undo_command: Command | None = None
def set_command(self, slot: int, on_cmd: Command, off_cmd: Command) -> None:
"""Client plugs commands into specific slots."""
self._on_commands[slot] = on_cmd
self._off_commands[slot] = off_cmd
def on_button_pressed(self, slot: int) -> None:
"""Execute the ON command for this slot."""
cmd = self._on_commands[slot]
if cmd:
cmd.execute()
self._undo_command = cmd
def off_button_pressed(self, slot: int) -> None:
"""Execute the OFF command for this slot."""
cmd = self._off_commands[slot]
if cmd:
cmd.execute()
self._undo_command = cmd
def undo_button_pressed(self) -> None:
"""Undo the last executed command."""
if self._undo_command:
self._undo_command.undo()# Client: create receiver, commands, and wire them into the remote
light = Light("Kitchen")
light_on = LightOnCommand(light)
light_off = LightOffCommand(light)
remote = RemoteControl()
remote.set_command(0, light_on, light_off)
# Invoker presses buttons -- doesn't know about Light at all
remote.on_button_pressed(0)
# Kitchen light is ON
remote.off_button_pressed(0)
# Kitchen light is OFF
remote.on_button_pressed(0)
# Kitchen light is ON
# Undo reverts the last action
remote.undo_button_pressed()
# Kitchen light is OFFCommand (interface)
├── execute()
└── undo()
LightOnCommand (ConcreteCommand)
├── self._light: Light
├── execute() → self._light.on()
└── undo() → self._light.off()
LightOffCommand (ConcreteCommand)
├── self._light: Light
├── execute() → self._light.off()
└── undo() → self._light.on()
RemoteControl (Invoker)
├── _on_commands: list[Command]
├── _off_commands: list[Command]
├── _undo_command: Command
└── stores last command for undo
Light (Receiver)
├── on()
└── off()
| Feature | How Command makes it work |
|---|---|
| Parameterization | Pass a command object to a method instead of hardcoding behavior |
| Queuing | Store commands in a deque and execute them in order |
| Logging | Serialize commands to disk, replay on restart |
| Undo | Call undo() on the last executed command |
| Macros | Combine commands: MacroCommand executes a list of commands |
- In Python, functions are already first-class objects, so you can sometimes use plain callables instead of full Command classes. But Command objects win when you need
undo()or want to serialize the command state. functools.partialcan be used for simple command parameterization- Python's
undoframework can use a stack:self._undo_stack.append(cmd) types.MethodTypelets you bind receiver + method dynamically- Modern Python:
callableobjects andcollections.dequework well together for command queuing
- Overusing Command: if you only need a simple callback, a plain function is cleaner
- Memory leaks: keeping commands alive for unlimited undo consumes memory. Limit stack size or implement "checkpoint" undo.
- Macro commands: when combining commands in
MacroCommand, remember to undo in reverse order
Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.
Joe's duck simulation needs duck behavior. All he has is turkeys. Turkey says gobble(), duck says quack(). Turkey does short flights, duck does long ones. The interfaces are incompatible. Creating a new turkey class that extends duck doesn't make sense. Solution: wrap the turkey in an adapter that looks like a duck.
- Target (
Duck): the interface the client expects - Adaptee (
Turkey): the existing class with a different interface - Adapter (
TurkeyAdapter): translates Target calls to Adaptee calls
from abc import ABC, abstractmethod
# ---- Target: what the client expects ----
class Duck(ABC):
"""The interface the rest of the code talks to."""
@abstractmethod
def quack(self) -> None:
pass
@abstractmethod
def fly(self) -> None:
pass
class MallardDuck(Duck):
def quack(self) -> None:
print("Quack")
def fly(self) -> None:
print("I'm flying a long distance")
# ---- Adaptee: what we actually have ----
class Turkey(ABC):
"""A different interface from a different codebase."""
@abstractmethod
def gobble(self) -> None:
pass
@abstractmethod
def fly(self) -> None:
pass
class WildTurkey(Turkey):
def gobble(self) -> None:
print("Gobble gobble")
def fly(self) -> None:
print("I'm flying a short distance (only ~5 feet)")
# ---- Adapter: makes Turkey look like Duck ----
class TurkeyAdapter(Duck):
"""Wraps a Turkey so it satisfies the Duck interface."""
def __init__(self, turkey: Turkey):
self._turkey = turkey
def quack(self) -> None:
# Translate Duck.quack → Turkey.gobble
self._turkey.gobble()
def fly(self) -> None:
# Duck.fly = long distance, but turkey only flies short.
# Simulate one duck flight by making the turkey fly 5 times.
for _ in range(5):
self._turkey.fly()
# ---- Client code: works with Ducks ONLY ----
def duck_simulation(duck: Duck) -> None:
print("--- Duck Test ---")
duck.quack()
duck.fly()
# Works with a real duck
duck_simulation(MallardDuck())
# --- Duck Test ---
# Quack
# I'm flying a long distance
# Works with a turkey wrapped in an adapter
duck_simulation(TurkeyAdapter(WildTurkey()))
# --- Duck Test ---
# Gobble gobble
# I'm flying a short distance (x5)Duck (Target - interface the client expects)
├── quack()
└── fly()
MallardDuck (ConcreteTarget, implements Duck)
WildTurkey (Adaptee - incompatible interface)
├── gobble()
└── fly() (short distance)
TurkeyAdapter (Adapter - implements Duck, wraps WildTurkey)
├── self._turkey: Turkey
├── quack() → self._turkey.gobble()
└── fly() → self._turkey.fly() x 5
Both wrap an object. The key difference is the goal:
| Aspect | Adapter | Decorator |
|---|---|---|
| Purpose | Change the interface | Same interface, add behavior |
| Transparency | Client knows something different is being adapted | Client doesn't need to know |
| Composition | Wraps incompatible object | Wraps compatible object to add features |
Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.
Joe's home theater has 8 components: amplifier, tuner, streaming player, DVD player, projector, screen, theater lights, and popcorn popper. To watch a movie, you need to turn on 8 devices in the right order. That's 10+ method calls. A Facade collapses all of this into one method: watch_movie().
class Amplifier:
def on(self) -> None: print("Amplifier is on")
def off(self) -> None: print("Amplifier is off")
def set_surround_sound(self) -> None: print("Amplifier surround sound")
def set_volume(self, level: int) -> None:
print(f"Amplifier volume set to {level}")
class Tuner:
def on(self) -> None: print("Tuner is on")
def off(self) -> None: print("Tuner is off")
def set_fm(self) -> None: print("Tuner FM mode")
class StreamingPlayer:
def on(self) -> None: print("Streaming Player is on")
def off(self) -> None: print("Streaming Player is off")
def set_hdmi(self, title: str) -> None:
print(f"Streaming Player HD mode for '{title}'")
class DVDPlayer:
def on(self) -> None: print("DVD Player is on")
def off(self) -> None: print("DVD Player is off")
def play(self, title: str) -> None:
print(f"DVD Player playing '{title}'")
def stop(self) -> None: print("DVD Player stop")
class Projector:
def on(self) -> None: print("Projector is on")
def off(self) -> None: print("Projector is off")
def wide_screen_mode(self) -> None:
print("Projector 16x9 wide screen mode")
class TheaterLights:
def on(self) -> None: print("Lights on")
def off(self) -> None: print("Lights off")
def dim(self, level: int) -> None:
print(f"Lights dimming to {level}%")
class Screen:
def up(self) -> None: print("Screen going up")
def down(self) -> None: print("Screen going down")
class PopcornPopper:
def on(self) -> None: print("Popcorn popper is on")
def off(self) -> None: print("Popcorn popper is off")
def pop(self) -> None: print("Popcorn is popping!")class HomeTheaterFacade:
"""Facade: one simple method to watch a movie."""
def __init__(self):
# Create all subsystem components internally
self._amp = Amplifier()
self._tuner = Tuner()
self._streaming_player = StreamingPlayer()
self._dvd = DVDPlayer()
self._projector = Projector()
self._screen = Screen()
self._lights = TheaterLights()
self._popper = PopcornPopper()
def watch_movie(self, title: str) -> None:
"""Instead of 10+ individual calls, this is all you need."""
print("--- Getting ready to watch a movie ---")
self._popper.on()
self._popper.pop()
self._lights.dim(10)
self._screen.down()
self._projector.on()
self._projector.wide_screen_mode()
self._amp.on()
self._amp.set_surround_sound()
self._amp.set_volume(5)
self._dvd.on()
self._dvd.play(title)
def end_movie(self) -> None:
"""Shut everything down in reverse order."""
print("--- Shutting movie theater down ---")
self._dvd.stop()
self._dvd.off()
self._amp.off()
self._projector.off()
self._screen.up()
self._lights.on()
self._popper.off()
# Client: ONE method call instead of 10+
theater = HomeTheaterFacade()
theater.watch_movie("The Matrix")
# --- Getting ready to watch a movie ---
# Popcorn popper is on
# Popcorn is popping!
# Lights dimming to 10%
# Screen going down
# Projector is on
# Projector 16x9 wide screen mode
# Amplifier is on
# Amplifier surround sound
# Amplifier volume set to 5
# DVD Player is on
# DVD Player playing 'The Matrix'
theater.end_movie()
# --- Shutting movie theater down ---| Aspect | Adapter | Facade |
|---|---|---|
| Purpose | Make incompatible interfaces work together | Simplify a complex subsystem |
| Interfaces | One-to-one translation | Many-to-one simplification |
| Client | Client expects Target interface | Client doesn't use the subsystem directly |
| Complexity | Reconciles two interfaces | Hides many interfaces behind one |
- Python's duck typing often eliminates the need for adapters -- if an object has the right methods, Python accepts it
typing.Protocolformalizes duck typing for type checkers (mypy, pyright)- Facade is common in frameworks:
app.run()in Flask is a facade over WSGI, routing, middleware - Python's
withstatement (context managers) is a facade over__enter__and__exit__methods
Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm's structure.
Both coffee and tea preparation follow the same steps: boil water, brew, pour in cup, add condiments. But the details of brewing and condiments differ. Without Template Method, you'd duplicate the 4-step algorithm in both classes. Duplicated code = duplicated bugs when you need to change the algorithm.
- Template Method (
prepare_recipe): defines the algorithm skeleton. Subclasses should NOT override this - Abstract Methods (
brew,add_condiments): subclasses MUST implement these - Concrete Methods (
boil_water,pour_in_cup): same implementation for all subclasses - Hooks (
customer_wants_condiments): optional methods subclasses may override to influence the algorithm
from abc import ABC, abstractmethod
class CaffeineBeverage(ABC):
"""Defines the algorithm skeleton. Subclasses fill in the blanks."""
# Template Method -- the fixed algorithm skeleton
def prepare_recipe(self) -> None:
self.boil_water()
self.brew()
self.pour_in_cup()
self.add_condiments()
# Concrete step -- identical for all beverages
def boil_water(self) -> None:
print("Boiling water")
# Abstract steps -- each subclass implements differently
@abstractmethod
def brew(self) -> None:
pass
@abstractmethod
def add_condiments(self) -> None:
pass
# Concrete step -- identical for all beverages
def pour_in_cup(self) -> None:
print("Pouring into cup")
# Hook -- optional, subclasses can override to change algorithm flow
def customer_wants_condiments(self) -> bool:
return True
class Coffee(CaffeineBeverage):
def brew(self) -> None:
print("Dripping coffee through filter")
def add_condiments(self) -> None:
print("Adding sugar and milk")
class Tea(CaffeineBeverage):
def brew(self) -> None:
print("Steeping the tea bag")
def add_condiments(self) -> None:
print("Adding lemon")
class CoffeeWithHook(CaffeineBeverage):
"""Shows hook methods in action."""
def brew(self) -> None:
print("Dripping coffee through filter")
def add_condiments(self) -> None:
print("Adding sugar and milk")
def customer_wants_condiments(self) -> bool:
# Ask the customer interactively
return input("Do you want sugar and milk? (y/n)").lower() == "y"
# Override template to use the hook
def prepare_recipe(self) -> None:
self.boil_water()
self.brew()
self.pour_in_cup()
if self.customer_wants_condiments():
self.add_condiments()
# Testing
print("--- Coffee ---")
coffee = Coffee()
coffee.prepare_recipe()
# Boiling water
# Dripping coffee through filter
# Pouring into cup
# Adding sugar and milk
print("\n--- Tea ---")
tea = Tea()
tea.prepare_recipe()
# Boiling water
# Steeping the tea bag
# Pouring into cup
# Adding lemonTemplate Method is a form of Inversion of Control. The base class controls the algorithm and calls into the subclass. The subclass doesn't call the base class -- it just implements what it needs when the base class calls it. This is the Hollywood Principle: "Don't call us, we'll call you."
| Aspect | Template Method | Strategy |
|---|---|---|
| Mechanism | Uses inheritance (subclass overrides) | Uses composition (context delegates) |
| Flexibility | Algorithm structure fixed at compile time | Behavior can change at runtime |
| Open/Closed | New subclasses add new behavior | New strategy objects add new behavior |
| Override | "Freezes" template with final/no override |
Cannot "freeze" -- always swappable |
- Python doesn't have
finalmethods, but you can use@finalfromtyping(3.8+) to signal that a method shouldn't be overridden - Multiple inheritance and mixins are a Python alternative to Template Method for composing behaviors
abcmodule with@abstractmethodenforces that subclasses implement required methods
Hooks are methods that can be overridden by subclasses but don't have to be. They let subclasses influence the algorithm without being forced to provide an implementation. For example, customer_wants_condiments() in CoffeeWithHook returns True by default, but a subclass can override it to interact with the user.
Provide a way to access the elements of an aggregate object sequentially without exposing its internal representation.
The Pancake House stores its menu items in a Python list. The Diner stores its menu items in a fixed-size array. The Waitress class just wants to iterate through both without knowing how each menu stores its data. Iterator pattern abstracts the traversal mechanism.
- Iterator (interface): defines
__next__()and__iter__()protocol - Aggregate (interface): defines
__iter__()to return an iterator - Concrete Iterator: maintains current position through traversal
- Concrete Aggregate: returns its own iterator
In Python, the iterator pattern is BUILT INTO THE LANGUAGE. Two dunder methods are all you need:
__iter__(): return self or an iterator object__next__(): return the next element, or raiseStopIteration
class MenuItem:
def __init__(
self,
name: str,
description: str,
vegetarian: bool,
price: float,
):
self._name = name
self._description = description
self._vegetarian = vegetarian
self._price = price
def name(self) -> str:
return self._name
def is_vegetarian(self) -> bool:
return self._vegetarian
def __str__(self) -> str:
veg_tag = " (veg)" if self._vegetarian else ""
return (
f"{self._name}, ${self._price:.2f}{veg_tag}\n "
f"{self._description}"
)
class PancakeHouseMenu:
"""Stores items in a Python list -- naturally iterable."""
def __init__(self):
self._menu_items: list[MenuItem] = []
self._add("K&B Pancake Breakfast",
"Blueberry pancakes with scrambled eggs",
True, 2.99)
self._add("Regular Pancake Breakfast",
"Pancakes with fried eggs",
False, 2.99)
self._add("Blueberry Pancakes",
"Fresh blueberries with powdered sugar",
True, 3.49)
def _add(
self,
name: str,
description: str,
vegetarian: bool,
price: float,
) -> None:
self._menu_items.append(
MenuItem(name, description, vegetarian, price)
)
# Iterator: return an iterator over the internal list
def __iter__(self):
return iter(self._menu_items)
class DinerMenu:
"""Stores items in a fixed-size array (simulated with list + max cap)."""
MAX_ITEMS = 6
def __init__(self):
self._menu_items: list[MenuItem] = []
self._add("Vegetarian BLT",
"Bacon with lettuce and tomato",
True, 2.99)
self._add("BLT",
"Bacon with lettuce and tomato",
False, 2.99)
self._add("Soup of the day",
"A bowl of the soup of the day",
True, 3.29)
def _add(
self,
name: str,
description: str,
vegetarian: bool,
price: float,
) -> None:
if len(self._menu_items) < self.MAX_ITEMS:
self._menu_items.append(
MenuItem(name, description, vegetarian, price)
)
# Iterator: return an iterator over the internal list
def __iter__(self):
return iter(self._menu_items)
class Waitress:
"""Can iterate through ANY menu without knowing its internal storage."""
def __init__(
self,
pancake_menu: PancakeHouseMenu,
diner_menu: DinerMenu,
):
self._pancake_menu = pancake_menu
self._diner_menu = diner_menu
def print_menu(self) -> None:
print("=== BREAKFAST ===")
for item in self._pancake_menu:
print(f" {item}")
print()
print("=== DINNER ===")
for item in self._diner_menu:
print(f" {item}")
def print_vegetarian_menu(self) -> None:
print("=== VEGETARIAN MENU ===")
for menu in (self._pancake_menu, self._diner_menu):
for item in menu:
if item.is_vegetarian():
print(f" {item}")
# Testing: Waitress doesn't know how each menu stores items
pancake_menu = PancakeHouseMenu()
diner_menu = DinerMenu()
waitress = Waitress(pancake_menu, diner_menu)
waitress.print_menu()
waitress.print_vegetarian_menu()| Java | Python |
|---|---|
java.util.Iterator.hasNext() |
try/except StopIteration or next(it, default) |
java.util.Iterator.next() |
it.__next__() or next(it) |
java.util.Iterator.remove() |
Not part of Python iterator protocol |
java.lang.Iterable.iterator() |
object.__iter__() |
In Python, the for x in iterable: syntax handles all iterator mechanics. The pattern is so baked in that you rarely write explicit iterators.
iter(collection)calls collection's__iter__()next(iterator)calls iterator's__next__()- Generator functions (
yield) are implicit iterators itertoolsmodule provides iterator compositions:chain,zip,cycle,permutations
Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.
The menu has individual items (Pancakes, BLT) AND menu categories (Dinner Menu, Breakfast Menu). Each menu can contain items OR other menus (submenus). A naive design would treat items and menus separately -- but you want to traverse the menu tree uniformly, whether you're looking at a leaf item or a composite menu.
- Component (
MenuComponent): the interface common to both leaves and composites. Declaresadd(),remove(),get_child(),print() - Leaf (
MenuItem): has no children, implements basic behavior - Composite (
Menu): implements both component operations AND container operations (add,remove,get_child). Stores children and delegates to them
class MenuComponent:
"""The component interface -- shared by MenuItem and Menu."""
def add(self, component: "MenuComponent") -> None:
raise NotImplementedError("Operation not supported on MenuItem")
def remove(self, component: "MenuComponent") -> None:
raise NotImplementedError
def get_child(self, index: int) -> "MenuComponent":
raise NotImplementedError
def print(self, indent: int = 0) -> None:
raise NotImplementedError
# ---- Leaf: individual menu item ----
class MenuItem(MenuComponent):
def __init__(
self,
name: str,
description: str,
vegetarian: bool,
price: float,
):
self._name = name
self._description = description
self._vegetarian = vegetarian
self._price = price
def print(self, indent: int = 0) -> None:
print(f"{' ' * indent}{self._name}, ${self._price:.2f} -- {self._description}")
if self._vegetarian:
print(f"{' ' * indent} (vegetarian)")
# ---- Composite: a menu can contain items AND other menus ----
class Menu(MenuComponent):
"""Recursive container: holds MenuComponent children."""
def __init__(self, name: str, description: str):
self._name = name
self._description = description
self._children: list[MenuComponent] = []
def add(self, component: MenuComponent) -> None:
self._children.append(component)
def remove(self, component: MenuComponent) -> None:
self._children.remove(component)
def get_child(self, index: int) -> MenuComponent:
return self._children[index]
def print(self, indent: int = 0) -> None:
print(f"\n{' ' * indent}{self._name}, {self._description}")
print(f"{' ' * indent}{'--' * (15 + indent)}")
for child in self._children:
# Delegate to each child -- the child decides how to print
child.print(indent + 2)
# ---- Building the menu tree ----
all_menus = Menu("ALL MENUS", "All menus combined")
breakfast = Menu("BREAKFAST", "Breakfast items")
breakfast.add(MenuItem("K&B Pancakes", "With scrambled eggs", True, 2.99))
breakfast.add(MenuItem("Waffles", "With fresh berries", True, 3.59))
dinner = Menu("DINNER", "Dinner items")
dinner.add(MenuItem("BLT", "Bacon with lettuce and tomato", False, 2.99))
dinner.add(MenuItem("Spaghetti", "With meat sauce", False, 3.29))
# You can nest menus deeper
kids_menu = Menu("KIDS MENU", "Kid-friendly items")
kids_menu.add(MenuItem("Chicken Tenders", "With fries", False, 4.99))
dinner.add(kids_menu) # Submenu inside dinner menu
all_menus.add(breakfast)
all_menus.add(dinner)
# The recursive print traverses the entire tree uniformly:
all_menus.print()
"""
ALL MENUS, All menus combined
----------------------------------------------------------------------
BREAKFAST, Breakfast items
--------------------------------------------------
K&B Pancakes, $2.99 -- With scrambled eggs
(vegetarian)
Waffles, $3.59 -- With fresh berries
(vegetarian)
DINNER, Dinner items
--------------------------------------------------
BLT, $2.99 -- Bacon with lettuce and tomato
Spaghetti, $3.29 -- With meat sauce
KIDS MENU, Kid-friendly items
-------------------------------------------
Chicken Tenders, $4.99 -- With fries
"""MenuComponent (Component interface)
├── add(), remove(), get_child(), print()
├── MenuItem (Leaf)
│ ├── name, description, price, vegetarian
│ └── print() → prints own info
│
└── Menu (Composite)
├── name, description
├── self._children: list[MenuComponent]
├── add() → append to _children
├── remove() → remove from _children
└── print() → print self, then print each child recursively
Composites have a design choice:
- Safe Composite: Only the Composite declares
add(),remove(),get_child(). Leaf doesn't -- clients must check type before calling container methods (avoids runtime errors) - Transparent Composite (used here): Component declares ALL methods, including
add(),remove(). Leaf raisesNotImplementedError. Clients call any method without checking type (simpler client code, but risks runtime errors)
__iter__on the Composite lets you usefor child in menu:naturally- The
any()andall()functions work well with iterators in composite pattern - Recursive depth: Python's default recursion limit (~1000) is usually fine for menus, but deep trees may need
sys.setrecursionlimit() - Consider
dataclassfor MenuItem to reduce boilerplate code
all_menus = Menu("ALL MENUS", "All menus combined") breakfast = Menu("BREAKFAST", "Breakfast") dinner = Menu("DINNER", "Dinner")
breakfast.add(MenuItem("Pancakes", "Con huevos", True, 2.99)) breakfast.add(MenuItem("Waffles", "Con toppings", True, 3.59)) dinner.add(MenuItem("BLT", "Bacon sandwich", False, 2.99)) dinner.add(MenuItem("Spaghetti", "Con salsa de carne", False, 3.29))
all_menus.add(breakfast) all_menus.add(dinner) all_menus.print()
---
## Chapter 10: Command Pattern (Macros and Undo)
### MacroCommand: Combining Commands
A `MacroCommand` treats multiple commands as one. When you call `execute()`, it runs all commands in sequence. When you call `undo()`, it undoes them in reverse order:
```python
from collections import deque
class MacroCommand(Command):
"""Composite of commands: executes a batch as one."""
def __init__(self, name: str, commands: list[Command] | None = None):
self._name = name
self._commands: list[Command] = commands or []
def add(self, command: Command) -> None:
self._commands.append(command)
def execute(self) -> None:
for cmd in self._commands:
cmd.execute()
def undo(self) -> None:
"""Undo in reverse order (last executed = first undone)."""
for cmd in reversed(self._commands):
cmd.undo()
# Building a "Movie Night" macro
movie_night = MacroCommand("Movie Night")
movie_night.add(light_on)
movie_night.add(lights_dim)
movie_night.add(projector_on)
movie_night.add(dvd_on)
movie_night.add(amp_on_with_surround)
movie_night.add(popcorn_on)
# Execute all at once
movie_night.execute()
# Living Room light is on
# Lights dimming to 10%
# Projector 16x9 wide screen mode
# DVD Player playing 'The Matrix'
# Amplifier surround sound
# Popcorn is popping!
# Undo everything in reverse
movie_night.undo()
# Popcorn popper is off
# Amplifier is off
# DVD Player stop
# Projector is off
# Lights on
# Living Room light is off
Building on Chapter 6, the RemoteControl with undo stores the last executed command:
class RemoteControlWithUndo:
"""Remote control that tracks the last command for undo."""
def __init__(self, num_slots: int = 7):
self._on_commands: list[Command | None] = [None] * num_slots
self._off_commands: list[Command | None] = [None] * num_slots
self._undo_stack: deque[Command] = deque()
def set_command(self, slot: int, on_cmd: Command, off_cmd: Command) -> None:
self._on_commands[slot] = on_cmd
self._off_commands[slot] = off_cmd
def on_button_pressed(self, slot: int) -> None:
cmd = self._on_commands[slot]
if cmd:
cmd.execute()
self._undo_stack.append(cmd)
def undo_button_pressed(self) -> None:
if self._undo_stack:
cmd = self._undo_stack.pop()
cmd.undo()
# Testing undo with stack
remote = RemoteControlWithUndo()
light = Light("Kitchen")
remote.set_command(0, LightOnCommand(light), LightOffCommand(light))
remote.on_button_pressed(0) # Kitchen light is ON
remote.undo_button_pressed() # Kitchen light is OFF| Combination | What it gives you |
|---|---|
| Command + Stack | Full undo history (pop and undo) |
| Command + List | Macros and batch execution |
| Command + Queue | Asynchronous dispatch (producer/consumer) |
| Command + Iterator | Apply commands to every element in a collection |
| Command + Logging | Serialize commands to disk, replay on crash |
collections.dequeis the natural Python structure for undo stacks (O(1) append/pop both ends)asyncio.Queueworks well for queuing commands in async applications- Command objects can be serialized with
pickleorjsonfor logging/replay functools.partial(fn, *args)creates simple command-like objects from functions
Provide a surrogate or placeholder for another object to control access to it.
Proxy is like a receptionist in a corporate office. The real subject (the CEO) is busy and expensive. The proxy checks if your request is worth forwarding. If you only need basic info (the CEO's email address), the proxy handles it directly. If you need to meet, the proxy arranges the meeting.
There are 4 main proxy types in the GoF book:
| Type | Controls | Example |
|---|---|---|
| Virtual Proxy | Expensive object creation | Lazy image loading |
| Protection Proxy | Access based on permissions | Security checks |
| Smart Reference | Extra operations on access | Logging, reference counting, caching |
| Remote Proxy | Network communication (stub) | RPC, distributed systems |
A virtual proxy defers the creation of a costly object until it's actually needed:
from abc import ABC, abstractmethod
class Image(ABC):
"""The interface the client talks to."""
@abstractmethod
def display(self) -> None:
pass
class RealImage(Image):
"""Expensive to create: loads from disk/network."""
def __init__(self, filename: str, width: int, height: int):
self._filename = filename
self._width = width
self._height = height
# Expensive operation: would load megabytes of pixel data
print(f"Loading '{filename}' ({width}x{height}) from disk... (expensive)")
def display(self) -> None:
print(f"Displaying '{self._filename}' at {self._width}x{self._height}")
class ImageProxy(Image):
"""Creates the RealImage only when display() is actually called."""
def __init__(self, filename: str, width: int, height: int):
self._filename = filename
self._width = width
self._height = height
self._real_image: RealImage | None = None
def display(self) -> None:
# Lazy creation: only build RealImage on first access
if self._real_image is None:
self._real_image = RealImage(
self._filename, self._width, self._height
)
self._real_image.display()
# Testing: the proxy is cheap to create
image: Image = ImageProxy("photo.jpg", 3000, 4000)
# No disk load yet -- the proxy is a tiny placeholder
# The expensive load only happens when you call display()
print("About to show image...")
image.display()
# Loading 'photo.jpg' (3000x4000) from disk... (expensive)
# Displaying 'photo.jpg' at 3000x4000
# Second call uses cached RealImage
image.display()
# Displaying 'photo.jpg' at 3000x4000 (no re-load)A protection proxy checks permissions before delegating to the real object:
class Document(ABC):
@abstractmethod
def read(self) -> None:
pass
@abstractmethod
def write(self, content: str) -> None:
pass
class RealDocument(Document):
def __init__(self, content: str):
self._content = content
def read(self) -> None:
print(f"Content: {self._content}")
def write(self, content: str) -> None:
self._content = content
print("Document updated")
class DocumentProxy(Document):
"""Checks permissions before allowing operations."""
def __init__(
self,
real_document: RealDocument,
permissions: set[str],
):
self._real_document = real_document
self._permissions = permissions
def read(self) -> None:
if "READ" in self._permissions:
self._real_document.read()
else:
raise PermissionError("Read access denied")
def write(self, content: str) -> None:
if "WRITE" in self._permissions:
self._real_document.write(content)
else:
raise PermissionError("Write access denied")
# Testing
doc = RealDocument("Top secret memo")
# Read-only user
read_only = DocumentProxy(doc, {"READ"})
read_only.read() # Content: Top secret memo
read_only.write("Hacked!") # PermissionError: Write access denied
# Admin user
admin = DocumentProxy(doc, {"READ", "WRITE"})
admin.read() # Content: Top secret memo
admin.write("Updated by admin") # Document updatedA smart reference intercepts every access to add behavior like logging, caching, or reference counting:
class SmartReferenceProxy(Document):
"""Tracks how many times the document is read and written."""
def __init__(self, real_document: RealDocument):
self._real_document = real_document
self._read_count = 0
self._write_count = 0
def read(self) -> None:
self._read_count += 1
print(f"[Access #{self._read_count}] Reading document...")
self._real_document.read()
def write(self, content: str) -> None:
self._write_count += 1
print(f"[Access #{self._write_count}] Writing to document...")
self._real_document.write(content)
def get_stats(self) -> str:
return f"Reads: {self._read_count}, Writes: {self._write_count}"
# Testing
smart = SmartReferenceProxy(RealDocument("Hello"))
smart.read() # [Access #1] Reading document... Content: Hello
smart.read() # [Access #2] Reading document... Content: Hello
smart.write("World") # [Access #1] Writing to document... Document updated
print(smart.get_stats()) # Reads: 2, Writes: 1Both wrap an object, but with different goals:
| Proxy | Decorator |
|---|---|
| Controls access to the real subject | Adds responsibilities transparently |
| Usually one per subject | Multiple per object (stacking) |
| Often invisible or intentional | Always intentional |
| Protects the real object | Extends the wrapped object |
- Python properties (
@property) act as smart reference proxies for attributes functools.lru_cacheis a virtual proxy for function results (lazy caching)__getattr__lets you implement transparent proxy forwarding on any classtypes.SimpleNamespacecan stand in as a placeholder proxycontextlib.contextmanagerproxies resource acquisition and cleanup
A compound pattern combines two or more patterns to solve a general, recurring design problem. It's more than implementing individual patterns in the same class -- it's about how the patterns work together as a system.
MVC is the classic compound pattern. It combines Observer and Strategy:
- Model = Subject (Observer Pattern): holds the application data, notifies observers when data changes
- View = Observer (Observer Pattern): displays the model's state, receives notifications when model changes
- Controller = Strategy: changes behavior dynamically to respond to user input
# Weather Data acts as the Model (Subject)
class WeatherData:
"""MVC Model: holds state and notifies views."""
def __init__(self):
self._observers: list[Observer] = []
self._temperature: float = 0.0
self._humidity: float = 0.0
self._pressure: float = 0.0
def register_observer(self, observer: Observer) -> None:
self._observers.append(observer)
def notify_observers(self) -> None:
for obs in self._observers:
obs.update(self._temperature, self._humidity, self._pressure)
def set_measurements(
self, temp: float, humidity: float, pressure: float
) -> None:
self._temperature = temp
self._humidity = humidity
self._pressure = pressure
# Observer: notify all views when model changes
self.notify_observers()Model (WeatherData, uses Observer pattern as Subject)
└── notifies all registered observers on change
View (CurrentConditionsDisplay, uses Observer pattern as Observer)
└── receives data from model, renders display
Controller (Strategy objects, uses Strategy pattern)
└── intercepts user input, updates model, which triggers view update
Flow: User Action → Controller → Model.change() → Observer.notify() → Views.update()
The menu system from Chapters 9 combines three patterns:
- Composite: Menu tree (MenuItem leaves, Menu composites)
- Iterator: Traverse the menu tree uniformly (regardless of leaf/composite)
- Observer: Waitress gets updated when menu changes
class Flock:
"""Composite + Iterator: a flock contains individual guitarists."""
def __init__(self, name: str):
self._name = name
self._guitarists: list["Guitarist"] = []
def add(self, guitarist: "Guitarist") -> None:
self._guitarists.append(guitarist)
# Iterator: can iterate over flock like a list
def __iter__(self):
return iter(self._guitarists)
def __str__(self) -> str:
return f"Flock: {self._name} ({len(self._guitarists)} guitarists)"
class Guitarist:
"""An individual musician in the flock."""
def __init__(self, name: str):
self._name = name
def get_setlist(self) -> list[str]:
return f"{self._name}'s setlist".split()
def __str__(self) -> str:
return f"Guitarist: {self._name}"
# Usage: Composite treats Flock and Guitarists uniformly
flock = Flock("Guitar Flock")
flock.add(Guitarist("Jimi"))
flock.add(Guitarist("Eric"))
# Iterator: loop through all guitarists
for guitarist in flock:
print(guitarist) # Guitarist: Jimi, Guitarist: EricComposite: Menu tree hierarchy (Menu contains MenuItem and Menu)
└── Iterator: for item in menu: traverses all branches recursively
└── Observer: when menu data changes, notify the waitress display
- Python's
collections.abc.Iterable+__iter__makes Iterator trivial - MVC inspired web frameworks: Django (MTV), Flask (loose MVC), FastAPI
- Python's signal/event systems (Django signals, Celery tasks) are Observer-based
dataclass+__post_init__+ Observer is a common Python compound pattern for reactive data
Throughout the book, every pattern builds on these core principles:
| # | Principle | What it means |
|---|---|---|
| 1 | Encapsulate what varies | Identify the varying parts, extract them into their own class |
| 2 | Program to interfaces, not implementations | Use ABC/Protocol references, not concrete class references |
| 3 | Favor composition over inheritance | "Has-a" is more flexible than "is-a" |
| 4 | Open/Closed Principle | Open for extension, closed for modification |
| 5 | Dependency Inversion Principle | Both high and low level modules depend on abstractions |
| 6 | Principle of Least Knowledge | Only talk to your direct "friends" (methods you call) |
An anti-pattern is something that sounds like a good idea but leads to bad outcomes:
| Anti-Pattern | Description | Why it's bad |
|---|---|---|
| Golden Hammer | Using your favorite pattern/tool for everything | Forces solutions where they don't fit |
| God Class | One class that does everything | Violates single responsibility, impossible to test |
| Pattern Abuse | Using heavy patterns for simple problems | Creates unnecessary complexity |
| Shotgun Surgery | Changing many classes for one new feature | Indicates poor encapsulation |
| Feature Envy | A method that accesses more of another class than its own | Belongs in a different class |
| Data Class (Anemic Model) | Plain data with no behavior | Pushes logic to callers, scattering algorithms |
- Monkey patching at runtime -- changes class behavior unpredictably
import *-- pollutes namespace, unclear where names come from- Deep inheritance trees -- Python supports MRO (Method Resolution Order), but deep trees obscure behavior
- Global state without documentation -- makes testing fragile
- Principles first, patterns second: understand WHY before applying HOW
- Simple solution > complex pattern: if a few lines of code solve it, don't reach for a pattern
- Don't force patterns where they don't fit: patterns are tools, not laws
- Think in terms of trade-offs: every pattern has costs and benefits
- Patterns help communication: naming a pattern helps teams discuss design decisions
- Patterns evolve: what's a pattern today may become a built-in language feature tomorrow (Iterator →
__iter__in Python)
| Pattern | Use When You Need To... |
|---|---|
| Strategy | Swap behavior at runtime |
| Observer | Notify dependents of state changes |
| Decorator | Add behavior dynamically without subclassing |
| Factory Method | Defer object creation to subclasses |
| Abstract Factory | Create families of related objects |
| Singleton | Ensure exactly one instance (use sparingly) |
| Command | Encapsulate requests as objects (undo, queue, log) |
| Adapter | Make incompatible interfaces work together |
| Facade | Simplify a complex subsystem |
| Template Method | Define algorithm, let subclasses fill in details |
| Iterator | Traverse collections uniformly |
| Composite | Treat individual and group objects uniformly |
| Proxy | Control access to an object |