Skip to content

NicasioSirvent/design-patterns-python

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 

Repository files navigation

Design Patterns - Python Edition

Chapter 1: Strategy Pattern

Context: SimUDuck

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.

Solution: Strategy Pattern

Define a family of algorithms, encapsulate each one, and make them interchangeable at runtime.

Why does this matter?

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.

Key terminology

  • 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")

The Context: Duck composes behaviors

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:
        pass

Concrete Duck subclasses

Each 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")

Testing: runtime behavior change

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 >>

Structure diagram

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

How it works step by step

  1. Define a strategy interface for each behavior that varies (FlyBehavior, QuackBehavior)
  2. Write concrete strategy classes for each algorithm variant
  3. In the context (Duck), maintain a reference to the strategy interface (not a concrete class)
  4. When the behavior is needed, delegate to the strategy: self._fly_behavior.fly()
  5. Optionally expose setters to change the strategy at runtime

Design principles applied

  • Identify what varies and encapsulate it: fly and quack behavior vary, so each gets its own class hierarchy
  • Program to interfaces, not implementations: Duck holds FlyBehavior, not FlyWithWings
  • Favor composition over inheritance: a Duck has-a FlyBehavior instead of being-a FlyableDuck
  • Open/Closed Principle: you can add new behaviors (FlyRocketPowered) without modifying any existing class

Python-specific notes

  • ABC + abstractmethod from abc replaces Java's interface keyword for behavior contracts
  • The | None type 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

Chapter 2: Observer Pattern

Definition

Observer defines a one-to-many dependency between objects so that when one object changes state, all its dependents are automatically notified and updated.

Analogy: Newspaper Subscription

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()

Why does this matter?

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.

Key terminology

  • 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

Application: Weather Station

The weather subject maintains temperature, humidity, and pressure. Three display panels act as observers: current conditions, statistics, and forecast.

Step 1: Define the abstract interfaces

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:
        pass

Step 2: Concrete Subject -- WeatherData

WeatherData 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()

Step 3: Concrete Observers -- Display Panels

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!")

Testing: push/pull model

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 now

Structure diagram

Subject (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)

How it works: Push vs Pull

  • 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.

Design benefits

  • Loose coupling: WeatherData only knows it has a list of Observer interfaces. It doesn't know (or care) what concrete classes they are
  • Open/Closed: You can add new displays (e.g., HeatIndexDisplay) without modifying WeatherData
  • Broadcast communication: One notify_observers() call reaches every subscriber
  • Runtime flexibility: Observers subscribe/unsubscribe dynamically

Real-world Observer in Python

Python has built-in mechanisms that follow the same idea:

  • typing.Protocol for duck-typed observers
  • Decorator-based signal handling (like Django signals, SQLAlchemy events)
  • Asyncio event loops and callbacks
  • collections.abc observer-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)

Common pitfalls

  • 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

Chapter 3: Decorator Pattern

Definition

Dynamically attach additional responsibilities to an object. Decorators provide a flexible alternative to subclassing for extending functionality.

The Problem: Exploding Subclass Hierarchies

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.

Key terminology

  • 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

Application: Starbuzz Coffee

Step 1: The Component interface

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)."""
        pass

Step 2: Concrete Components -- Base Beverages

These 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.05

Step 3: The Abstract Decorator

CondimentDecorator 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 = beverage

Step 4: Concrete Decorators -- Condiments

Each 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.10

How wrapping works: the onion model

Each 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

Testing: unlimited combinations

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.24

Structure diagram

Beverage (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)

How it works step by step

  1. Create a concrete beverage: Espresso() -- this is the innermost layer
  2. Wrap it with decorators: Whip(Mocha(Espresso())) -- each wrapper adds its own cost/description
  3. When you call description() or cost() on the outermost decorator, it delegates inward through each layer
  4. You can stack decorators infinitely without creating new classes

Design principles applied

  • 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-specific notes

  • Python's built-in @decorator syntax is a different pattern entirely -- it's a compile-time transformation. The Decorator Pattern discussed here is about runtime object wrapping.
  • The functools.wraps decorator 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).

Common pitfalls

  • 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 than Mocha(Soy(beverage))
  • Double decorator: wrapping the same decorator twice (Mocha(Mocha(beverage))) works but should be intentional

Chapter 4: Factory Pattern

Definition

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.

The Problem: Hardcoded Object Creation

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.

Three levels of "Factory" in this chapter

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

Level 0: Simple Factory (NOT a formal pattern)

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 function

The issue: the PizzaStore is still coupled directly to a concrete CheesePizza class. Adding a new city style means modifying PizzaStore, violating open/closed.

Level 1: Factory Method Pattern

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

Testing Factory Method

# 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 boxing

Structure

Pizza (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

Level 2: Abstract Factory Pattern

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)

Structure

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 vs Abstract Factory: What's the difference?

  • 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.

Principle: Dependency Inversion Principle (DIP)

  • High-level modules should NOT depend on low-level modules. Both should depend on abstractions.
  • PizzaStore (high-level) depends on Pizza (abstraction), not on NYCheesePizza (low-level)
  • Details ("NYCheesePizza") depend on abstractions ("Pizza"), not the other way around

Evolution summary

  1. Simple Factory: a function that creates objects -- useful but not extensible
  2. Factory Method: subclass overrides a method to control object creation -- decouples the creator from the product
  3. Abstract Factory: factory object creates families of related objects -- ensures products work together correctly

Python-specific notes

  • Python doesn't have checked exceptions, so create_pizza() returning a wrong type will only show at runtime. Consider returning None or raising ValueError.
  • factory pattern in 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.Protocol can serve as a factory interface without requiring explicit inheritance.

Chapter 5: Singleton Pattern

Definition

Ensure a class has only one instance, and provide a global point of access to it.

Why does this matter?

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.

Common use cases

  • 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)

Implementation 1: Classic new override

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 object

How it works: __new__ is called before __init__. By controlling __new__, we return the cached instance instead of creating a new one.

Implementation 2: Singleton metaclass (reusable across classes)

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 instances

Implementation 3: Python modules as natural singletons (Pythonic!)

In 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 object

This is the most Pythonic Singleton. No special code needed -- the import system guarantees one instance.

Thread-safety considerations

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

Problems with Singleton

  • 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-specific notes

  • Python's GIL (Global Interpreter Lock) simplifies single-threaded singleton usage
  • threading.Lock is needed for true multi-threaded safety
  • Module-level import is the idiomatic Python approach for shared state
  • contextvars provides per-context (async) singletons for concurrent apps

Chapter 6: Command Pattern

Definition

Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

The Problem: Menu of Commands

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.

Key terminology

  • 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

Application: Remote Control

Step 1: The Command interface

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."""
        pass

Step 2: Concrete Commands

Each 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()

Step 3: The Invoker -- Remote Control

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()

Testing: the full chain

# 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 OFF

Structure diagram

Command (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()

Beyond the remote: what Command enables

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

Python-specific notes

  • 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.partial can be used for simple command parameterization
  • Python's undo framework can use a stack: self._undo_stack.append(cmd)
  • types.MethodType lets you bind receiver + method dynamically
  • Modern Python: callable objects and collections.deque work well together for command queuing

Common pitfalls

  • 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

Chapter 7: Adapter & Facade Patterns

Adapter Pattern

Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.

The Problem: 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.

Key terminology

  • 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)

Structure

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

Adapter vs Decorator: They Look Similar!

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

Facade Pattern

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.

The Problem: Too Many Parts to Wire Up

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().

Subsystem components:

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!")

The Facade: one simple method replaces 10+ calls

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 ---

Facade vs Adapter: What's the difference?

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-specific notes

  • Python's duck typing often eliminates the need for adapters -- if an object has the right methods, Python accepts it
  • typing.Protocol formalizes 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 with statement (context managers) is a facade over __enter__ and __exit__ methods

Chapter 8: Template Method Pattern

Definition

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.

The Problem: Duplicate Algorithm Skeleton

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.

Key terminology

  • 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 lemon

"Don't call us, we'll call you" -- Hollywood Principle

Template 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."

Template Method vs Strategy

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-specific notes

  • Python doesn't have final methods, but you can use @final from typing (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
  • abc module with @abstractmethod enforces that subclasses implement required methods

Hooks

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.


Chapter 9: Iterator & Composite Patterns

Iterator Pattern

Provide a way to access the elements of an aggregate object sequentially without exposing its internal representation.

The Problem: Traversal Without Exposure

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.

Key terminology

  • 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

Python's Built-in Iterator Protocol

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 raise StopIteration
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()

Comparison: Java Iterator vs Python Iterator

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.

Python-specific notes on Iterators

  • iter(collection) calls collection's __iter__()
  • next(iterator) calls iterator's __next__()
  • Generator functions (yield) are implicit iterators
  • itertools module provides iterator compositions: chain, zip, cycle, permutations

Composite Pattern

Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.

The Problem: Individual vs. Groups

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.

Key terminology

  • Component (MenuComponent): the interface common to both leaves and composites. Declares add(), 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
"""

Structure

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

Safety vs Transparency

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 raises NotImplementedError. Clients call any method without checking type (simpler client code, but risks runtime errors)

Python-specific notes

  • __iter__ on the Composite lets you use for child in menu: naturally
  • The any() and all() 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 dataclass for 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()

ALL MENUS, All menus combined

--------------------

BREAKFAST, Breakfast

--------------------

Pancakes), $2.99 -- Con huevos

(v)

Waffles), $3.59 -- Con toppings

(v)

DINNER, Dinner

--------------------

BLT), $2.99 -- Bacon sandwich

Spaghetti), $3.29 -- Con salsa de carne


---

## 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

Remote Control with Undo

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

Command Pattern: Related Combinations

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

Python-specific notes

  • collections.deque is the natural Python structure for undo stacks (O(1) append/pop both ends)
  • asyncio.Queue works well for queuing commands in async applications
  • Command objects can be serialized with pickle or json for logging/replay
  • functools.partial(fn, *args) creates simple command-like objects from functions


Chapter 11: Proxy Pattern

Definition

Provide a surrogate or placeholder for another object to control access to it.

The Problem: Controlling Access

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.

All Proxy Types

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

Type 1: Virtual Proxy -- Lazy Instantiation

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)

Type 2: Protection Proxy -- Access Control

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 updated

Type 3: Smart Reference -- Extra Operations

A 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: 1

Comparison: Proxy vs Decorator

Both 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-specific notes

  • Python properties (@property) act as smart reference proxies for attributes
  • functools.lru_cache is a virtual proxy for function results (lazy caching)
  • __getattr__ lets you implement transparent proxy forwarding on any class
  • types.SimpleNamespace can stand in as a placeholder proxy
  • contextlib.contextmanager proxies resource acquisition and cleanup


Chapter 12: Compound Patterns

What is a Compound Pattern?

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.

Example 1: MVC (Model-View-Controller)

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()

How MVC works together

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()

Example 2: Observer + Iterator + Composite = Menu Display

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: Eric

How the three patterns work together

Composite: 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-specific notes

  • 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

Chapter 13: Principles & Anti-Patterns

Design Principles Recap

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)

Anti-Patterns

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

Python-specific anti-patterns to avoid

  • 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

The Zen of Design Patterns

  • 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 summary cheat sheet

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

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors