<< Home

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"})
💡 Build chains from configuration to turn features on or off without changing code.

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()
⚠️ If you need 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({})
💡 For larger grammars, use parser generators; hand-built trees get complex once you add precedence, associativity, and {args …} handling.

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")
⚠️ A fat mediator can become a god object. Split responsibilities once rules accumulate.

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)
💡 Compress or diff large snapshots to control memory usage when you keep many mementos.

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)
⚠️ Beware update storms. Debounce notifications or batch changes to avoid thrashing observers.

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()
💡 Add a simple table for transitions if states grow; map (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])
⚠️ Too many tiny strategies can be overkill. Prefer plain functions when you do not need polymorphism or state.

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()
💡 Add default no-op hooks such as 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())
⚠️ Double-dispatch setups can feel heavy. If your language has pattern matching, consider matching on shape instead.

Variations

Use an Acceptor mixin to reduce boilerplate; add a default visit_Default(…) to handle unknown nodes gracefully.