Behavioral Design Patterns Cheat Sheet
Strategy, Observer, Command, and the rest of the Gang of Four behavioral patterns for organizing communication and responsibility between objects.
Strategy Pattern
Swapping interchangeable algorithms behind a common interface.
from abc import ABC, abstractmethodclass DiscountStrategy(ABC): @abstractmethod def apply(self, price: float) -> float: ...class NoDiscount(DiscountStrategy): def apply(self, price): return priceclass PercentageDiscount(DiscountStrategy): def __init__(self, percent): self.percent = percent def apply(self, price): return price * (1 - self.percent / 100)class Order: def __init__(self, strategy: DiscountStrategy): self.strategy = strategy # swap algorithm at runtime def total(self, price): return self.strategy.apply(price)order = Order(PercentageDiscount(20))order.total(100) # 80.0
Observer Pattern
Notifying subscribed objects automatically when state changes.
class Subject: def __init__(self): self._observers = [] def subscribe(self, observer): self._observers.append(observer) def notify(self, event): for obs in self._observers: obs.update(event)class Logger: def update(self, event): print(f"LOG: {event}")subject = Subject()subject.subscribe(Logger())subject.notify("order_created") # LOG: order_created
Command Pattern
Encapsulating a request as an object to support undo/redo.
from abc import ABC, abstractmethodclass Command(ABC): @abstractmethod def execute(self): ... @abstractmethod def undo(self): ...class AddTextCommand(Command): def __init__(self, document, text): self.document, self.text = document, text def execute(self): self.document.text += self.text def undo(self): self.document.text = self.document.text[:-len(self.text)]class Document: def __init__(self): self.text = ""doc = Document()history = []cmd = AddTextCommand(doc, "Hello")cmd.execute(); history.append(cmd)history.pop().undo() # reverts the change
Behavioral Pattern Catalog
One-line definitions of the classic Gang of Four behavioral patterns.
- Strategy- Encapsulate interchangeable algorithms behind a common interface and select one at runtime
- Observer- Define a one-to-many dependency so that when one object changes state, all dependents are notified
- Command- Encapsulate a request as an object, enabling undo/redo, queuing, and logging of operations
- Iterator- Provide sequential access to elements of a collection without exposing its underlying representation
- State- Let an object alter its behavior when its internal state changes, appearing to change its class
- Template Method- Define the skeleton of an algorithm in a base class, letting subclasses override individual steps
- Chain of Responsibility- Pass a request along a chain of handlers until one of them handles it
- Mediator- Centralize complex communication between objects in a mediator instead of direct object references
- Visitor- Separate an algorithm from the object structure it operates on by moving it into a visitor object
- Memento- Capture and externalize an object's internal state so it can be restored later without breaking encapsulation
State Pattern
Letting an object change its behavior by delegating to a swappable state object instead of branching on a status flag.
from abc import ABC, abstractmethodclass OrderState(ABC): @abstractmethod def next(self, order): ... @abstractmethod def name(self) -> str: ...class Placed(OrderState): def next(self, order): order.state = Shipped() def name(self): return "placed"class Shipped(OrderState): def next(self, order): order.state = Delivered() def name(self): return "shipped"class Delivered(OrderState): def next(self, order): raise RuntimeError("already delivered") def name(self): return "delivered"class Order: def __init__(self): self.state: OrderState = Placed() def advance(self): self.state.next(self) # object's behavior changes as self.state changesorder = Order()order.advance()order.state.name() # "shipped" — no if/elif chain on a status string anywhere
Template Method Pattern
Fixing the skeleton of an algorithm in a base class while letting subclasses override individual steps.
from abc import ABC, abstractmethodclass DataImporter(ABC): def run(self, path): """The template method — the algorithm's shape never changes.""" raw = self.read(path) records = self.parse(raw) self.validate(records) self.save(records) @abstractmethod def parse(self, raw): ... def read(self, path): with open(path) as f: return f.read() def validate(self, records): if not records: raise ValueError("no records parsed") @abstractmethod def save(self, records): ...class CsvImporter(DataImporter): def parse(self, raw): return [line.split(",") for line in raw.splitlines()] def save(self, records): print(f"saving {len(records)} rows to the database")
Chain of Responsibility Pattern
Passing a request along a chain of handlers, each deciding whether to process it or forward it.
from abc import ABC, abstractmethodclass Handler(ABC): def __init__(self): self._next: "Handler | None" = None def set_next(self, handler: "Handler") -> "Handler": self._next = handler return handler # enables chaining: a.set_next(b).set_next(c) def handle(self, request): if self._next: return self._next.handle(request) return Noneclass AuthHandler(Handler): def handle(self, request): if not request.get("token"): return "401 Unauthorized" return super().handle(request)class RateLimitHandler(Handler): def handle(self, request): if request.get("requests_this_minute", 0) > 100: return "429 Too Many Requests" return super().handle(request)class RouteHandler(Handler): def handle(self, request): return f"200 OK: routed to {request['path']}"chain = AuthHandler()chain.set_next(RateLimitHandler()).set_next(RouteHandler())chain.handle({"token": "abc", "path": "/orders"}) # "200 OK: routed to /orders"
Visitor Pattern
Adding a new operation over a fixed object hierarchy without modifying the hierarchy's classes.
from abc import ABC, abstractmethodclass Shape(ABC): @abstractmethod def accept(self, visitor): ...class Circle(Shape): def __init__(self, radius): self.radius = radius def accept(self, visitor): return visitor.visit_circle(self)class Square(Shape): def __init__(self, side): self.side = side def accept(self, visitor): return visitor.visit_square(self)class AreaVisitor: """New operation added without touching Circle or Square.""" def visit_circle(self, c): return 3.14159 * c.radius ** 2 def visit_square(self, s): return s.side ** 2class PerimeterVisitor: def visit_circle(self, c): return 2 * 3.14159 * c.radius def visit_square(self, s): return 4 * s.sideshapes = [Circle(3), Square(4)]areas = [shape.accept(AreaVisitor()) for shape in shapes] # [28.27, 16]
Behavioral Pattern Trade-offs
When to reach for each pattern and what it costs you in complexity.
- Strategy vs. plain function args- If the algorithm has no state and no config, a higher-order function is simpler than a Strategy class hierarchy
- Observer decoupling cost- Notification order between subscribers is undefined and debugging 'who fired this update' is harder; keep handlers idempotent
- Command memory cost- Undo stacks retain every executed command; use snapshots or a max history depth for long-running sessions
- Visitor breaks encapsulation- It requires exposing internal fields to the visitor and needs a new visit_x method whenever a new element type is added
- Mediator can become a God Object- Centralizing all communication risks recreating the tight coupling it was meant to remove; keep mediators thin
- Chain of Responsibility silent failures- If no handler in the chain processes the request, it disappears silently unless you add an explicit default/fallback handler
Reach for Strategy or State when you see a long if/elif chain switching on a type or mode flag — both replace conditional branching with polymorphism, but State is for behavior driven by an object's own lifecycle, while Strategy is for algorithms chosen by the caller.