Chapter 5: Behavioral Patterns
Behavioral patterns focus on how objects talk to each other. They help define responsibilities, communication styles, and the flow of work through your system. The goal is to reduce coupling while keeping intent clear and change-friendly.
Chain of Responsibility
Use Chain of Responsibility to pass a request along a linked list of handlers until one of them can deal with it. Each handler decides whether to handle or to forward, which lets you vary handling logic without large if cascades.
Intent
Decouple senders from receivers by giving multiple objects a chance to handle a request. The chain can be fixed or built at runtime.
Structure
Each handler keeps a reference to the next handler and exposes a method such as handle(request). If it cannot process the request, it forwards the request to next.
class Handler:
def __init__(self, next=None):
self.next = next
def handle(self, req):
if self.next:
return self.next.handle(req)
class AuthHandler(Handler):
def handle(self, req):
if req.get("user"):
return super().handle(req)
return "401 Unauthorized"
class LoggingHandler(Handler):
def handle(self, req):
print("log", req)
return super().handle(req)
class Endpoint(Handler):
def handle(self, req):
return "200 OK"
chain = AuthHandler(LoggingHandler(Endpoint()))
result = chain.handle({"path": "/api", "user": "robin"})
Considerations
Keep handlers focused. If the chain gets long, latency grows and debugging becomes harder.
Command
Command turns a request into a stand-alone object. You can queue, log, undo, and redo commands. The invoker triggers execute() on each command without knowing details.
Intent
Encapsulate an action and its parameters into an object with operations such as execute() and possibly undo().
Example
class TextDoc:
def __init__(self): self.text = ""
def insert(self, s): self.text += s
def delete_tail(self, n): self.text = self.text[:-n]
class InsertCommand:
def __init__(self, doc, s): self.doc, self.s = doc, s
def execute(self): self.doc.insert(self.s)
def undo(self): self.doc.delete_tail(len(self.s))
doc = TextDoc()
history = []
cmd = InsertCommand(doc, "Hello")
cmd.execute(); history.append(cmd)
history.pop().undo()
undo(), capture enough state to reverse the action. Memory growth can be an issue.
Variations
Use a command bus for async dispatch; add a CommandFactory for deserialization from logs.
Interpreter
Interpreter defines a grammar and evaluates sentences in that grammar. It works best for small languages such as filters, rules, or expression trees.
Intent
Represent grammar rules as classes, then interpret expressions composed from these classes. Terminals hold values, nonterminals combine expressions using interpret(context).
Example
class Num:
def __init__(self, value): self.value = value
def interpret(self, ctx): return self.value
class Add:
def __init__(self, left, right): self.left, self.right = left, right
def interpret(self, ctx): return self.left.interpret(ctx) + self.right.interpret(ctx)
expr = Add(Num(2), Add(Num(3), Num(4)))
result = expr.interpret({})
Considerations
Interpretation can be slow for deep trees. Consider a compiled form such as bytecode or a visit() that emits optimized steps.
Iterator
Iterator provides sequential access to the elements of a collection without exposing its internal representation. The client uses __iter__() and __next__() or similar protocol.
Intent
Traverse containers uniformly; support multiple simultaneous traversals; keep traversal logic out of the container.
Example
class Countdown:
def __init__(self, start): self.start = start
def __iter__(self):
n = self.start
while n > 0:
yield n
n -= 1
for n in Countdown(3):
print(n)
Considerations
Prefer lazy iteration for large data; add filtering and mapping by composing iterators.
Mediator
Mediator centralizes complex communication among colleagues. Each colleague talks to the mediator, which coordinates actions and routes messages.
Intent
Reduce many-to-many links among objects by moving coordination into a single Mediator.
Example
class Chat:
def __init__(self): self.users = []
def join(self, user): self.users.append(user)
def send(self, sender, msg):
for u in self.users:
if u is not sender:
u.receive(msg)
class User:
def __init__(self, name, chat): self.name, self.chat = name, chat; chat.join(self)
def send(self, msg): self.chat.send(self, f"{self.name}: {msg}")
def receive(self, msg): print(msg)
room = Chat()
a = User("Ada", room); b = User("Lin", room)
a.send("hi")
Variations
Use publish-subscribe so the mediator becomes a topic bus; colleagues subscribe to events instead of direct callbacks.
Memento
Memento captures and restores an object’s internal state without breaking encapsulation. The originator creates snapshots, the caretaker stores them.
Intent
Provide checkpoint and rollback. Useful for editors, games, and configuration changes.
Example
class Editor:
def __init__(self): self.text = ""; self.cursor = 0
def type(self, s): self.text = self.text[:self.cursor] + s + self.text[self.cursor:]; self.cursor += len(s)
def save(self): return (self.text, self.cursor)
def restore(self, m): self.text, self.cursor = m
ed = Editor()
ed.type("Hi"); m = ed.save()
ed.type(" there")
ed.restore(m)
Considerations
Decide who owns snapshot lifetimes. Unbounded caretakers can leak memory.
Observer
Observer defines a one-to-many dependency so when one object changes state, dependents are notified. Subjects expose subscribe() and notify().
Intent
Broadcast change without tight coupling. Observers decide what to do with updates.
Example
class Subject:
def __init__(self): self._obs = []
def subscribe(self, fn): self._obs.append(fn)
def set_value(self, v):
self.value = v
for fn in self._obs: fn(v)
s = Subject()
s.subscribe(lambda v: print("plot update", v))
s.subscribe(lambda v: print("cache write", v))
s.set_value(42)
Variations
Use event objects with {type: …, data: …} to version your protocol. Add weak references to avoid retention cycles.
State
State lets an object alter its behavior when its internal state changes. The context holds a reference to a state object; switching states swaps logic.
Intent
Represent state-specific behavior using separate classes that share a common interface such as handle().
Example
class Locked:
def handle(self, door): print("locked"); door.state = Unlocked()
class Unlocked:
def handle(self, door): print("open"); door.state = Locked()
class Door:
def __init__(self): self.state = Locked()
def press(self): self.state.handle(self)
d = Door(); d.press(); d.press()
(state, event) -> next to keep logic clear.
Considerations
Prefer small state classes. If states share heavy data, move it into the context to avoid duplication.
Strategy
Strategy defines a family of algorithms, encapsulates each one, and makes them interchangeable. The context selects a strategy that implements a common interface such as compute(input).
Intent
Swap algorithms without changing the client. Choose at runtime or via configuration.
Example
class SumStrategy:
def compute(self, data): return sum(data)
class MaxStrategy:
def compute(self, data): return max(data)
class Stats:
def __init__(self, strat): self.strat = strat
def run(self, data): return self.strat.compute(data)
s = Stats(SumStrategy()); s.run([1,2,3])
Variations
Use a registry keyed by name to look up strategies from config; switch to a composite strategy for pipelines.
Template Method
Template Method defines the skeleton of an algorithm in a base class and lets subclasses override steps. The template calls hooks like step1() and step2() in a fixed order.
Intent
Enforce a stable process while allowing variation at defined points. Keep the template simple and obvious.
Example
class Report:
def build(self):
self.header()
self.body()
self.footer()
def header(self): print("Header")
def body(self): raise NotImplementedError
def footer(self): print("Footer")
class SalesReport(Report):
def body(self): print("Sales …")
r = SalesReport(); r.build()
before() and after() so subclasses override only what they need.
Considerations
Prefer composition with Strategy if subclasses multiply. Inheritance can lock you into one axis of variation.
Visitor
Visitor separates an algorithm from the object structure it operates on. Each element provides accept(visitor), and the visitor implements typed methods such as visit_File().
Intent
Add operations to a stable set of element classes without editing them. The tradeoff is that adding a new element type requires touching all visitors.
Example
class File:
def __init__(self, name, size): self.name, self.size = name, size
def accept(self, v): return v.visit_File(self)
class Folder:
def __init__(self, name, children): self.name, self.children = name, children
def accept(self, v): return v.visit_Folder(self)
class SizeVisitor:
def visit_File(self, f): return f.size
def visit_Folder(self, d): return sum(c.accept(self) for c in d.children)
tree = Folder("root", [File("a.txt", 10), Folder("bin", [File("b", 5)])])
total = tree.accept(SizeVisitor())
Variations
Use an Acceptor mixin to reduce boilerplate; add a default visit_Default(…) to handle unknown nodes gracefully.