Module 09: Behavioral Patterns II
Why this matters
This module finishes the pattern catalog: Iterator, Chain of Responsibility, Mediator, Memento, and Visitor. After this, you have all 22 patterns this track covers (of the GoF's 23 — only Interpreter, which is genuinely rare outside building parsers/DSLs, is skipped) as concrete vocabulary. Every classic problem starting at module 12 will reuse this entire catalog freely — you'll be expected to recognize "oh, this needs a Chain of Responsibility" the same way you'd recognize a sorting problem needs a sort, not derive it from scratch under interview pressure.
Concepts
Iterator — walk a collection without exposing its internals
Problem it solves: let calling code step through a collection's elements one at a time, without knowing or caring whether it's backed by an array, a linked list, or something more exotic.
class BookShelf:
def __init__(self):
self._books = []
def add(self, book):
self._books.append(book)
def __iter__(self): # Python's iteration protocol — makes BookShelf directly iterable
return iter(self._books)
shelf = BookShelf()
shelf.add("Dune")
shelf.add("Neuromancer")
for book in shelf: # 'for' works because __iter__ exists — no exposed internals
print(book)
Python's iteration protocol (__iter__/__next__, or the simpler
yield-based generator form) is the Iterator pattern, built into the
language — you rarely hand-write a separate Iterator class in Python;
you implement the protocol on your own collection instead:
class BookShelf:
def __init__(self):
self._books = []
def add(self, book):
self._books.append(book)
def __iter__(self):
for book in self._books: # a generator-based iterator — even less ceremony
yield book
The honest takeaway: both languages have made Iterator a
first-class, built-in protocol precisely because it's that useful — in
real code you almost always implement __iter__/IEnumerable<T> on your
own type rather than hand-rolling a separate custom Iterator class from
scratch. Knowing this is the pattern (not "avoiding" it) is the actual
interview-relevant knowledge.
Chain of Responsibility — pass a request along a chain until someone handles it
Problem it solves: a request may need to be handled by one of several possible handlers, and the sender shouldn't need to know which one, or how many there are.
from abc import ABC, abstractmethod
class Approver(ABC):
def __init__(self):
self._next: Approver | None = None
def set_next(self, next_approver: "Approver") -> "Approver":
self._next = next_approver
return next_approver # returning it lets you chain .set_next() calls too
@abstractmethod
def approve(self, amount: float): ...
class TeamLead(Approver):
def approve(self, amount):
if amount <= 1000:
print(f"TeamLead approved {amount}")
elif self._next:
self._next.approve(amount) # can't handle it — pass it along the chain
else:
print("No one could approve this")
class Manager(Approver):
def approve(self, amount):
if amount <= 10000:
print(f"Manager approved {amount}")
elif self._next:
self._next.approve(amount)
else:
print("No one could approve this")
class Director(Approver):
def approve(self, amount):
print(f"Director approved {amount}") # the end of the chain — handles everything left
lead = TeamLead()
lead.set_next(Manager()).set_next(Director())
lead.approve(500) # TeamLead approved 500
lead.approve(5000) # Manager approved 5000
lead.approve(50000) # Director approved 50000 — the CALLER never chose which approver handled it
A real design risk, named up front: if the last handler in the chain
doesn't unconditionally handle whatever's left (like Director does
above), a request that no handler recognizes silently vanishes with no
error and no log — always design an explicit fallback/terminal handler,
covered again in Common Mistakes.
Mediator — centralize chaotic many-to-many communication
Problem it solves: when many objects need to talk to each other directly, the web of references between them becomes unmanageable (every object coupled to every other). A Mediator centralizes that communication through one object, so participants only ever talk to it.
class ChatRoom: # the MEDIATOR
def show_message(self, sender: str, message: str):
print(f"[{sender}]: {message}")
class User:
def __init__(self, name: str, chat_room: ChatRoom):
self.name = name
self.chat_room = chat_room # User knows only the mediator — NOT every other User directly
def send(self, message: str):
self.chat_room.show_message(self.name, message)
room = ChatRoom()
alice = User("Alice", room)
bob = User("Bob", room)
alice.send("hey Bob!") # [Alice]: hey Bob!
bob.send("hey Alice!") # [Bob]: hey Alice! — Alice and Bob never hold a reference to EACH OTHER
Notice the resemblance to Observer (module 08) — both centralize communication through one object. The difference: Observer is a one-directional broadcast (subject → many observers, observers don't talk back through it to each other); Mediator is specifically about coordinating communication between peers that would otherwise need direct references to each other.
Memento — capture and restore state, without exposing internals
Problem it solves: you want undo/snapshot capability, without the object whose state is being saved having to expose its private internals to whoever manages the snapshots.
class EditorMemento: # an opaque snapshot — its contents are private to Editor
def __init__(self, content):
self._content = content # single underscore: "only Editor should read this" (module 01 convention)
class Editor: # the ORIGINATOR
def __init__(self):
self.content = ""
def type_text(self, text):
self.content += text
def save(self) -> EditorMemento:
return EditorMemento(self.content)
def restore(self, memento: EditorMemento):
self.content = memento._content # Editor is the only one that reaches into the memento
class History: # the CARETAKER — stores mementos, never reads/uses their content
def __init__(self):
self._snapshots = []
def push(self, memento):
self._snapshots.append(memento)
def pop(self):
return self._snapshots.pop() if self._snapshots else None
editor = Editor()
history = History()
editor.type_text("Hello")
history.push(editor.save()) # snapshot BEFORE the next change
editor.type_text(", world")
print(editor.content) # Hello, world
editor.restore(history.pop()) # undo back to the snapshot
print(editor.content) # Hello
The caretaker (History) deliberately never inspects or manipulates
a memento's contents — it just stores and returns opaque snapshots. Only
the originator (Editor) knows how to create and consume them. This
is what keeps Memento from breaking encapsulation the way exposing raw
internal state to an external undo-stack would.
Visitor — add new operations to a class hierarchy without modifying it
Problem it solves: you have a stable hierarchy of related classes
(from module 02's Shape example) and want to add a new operation
across all of them (export to JSON, compute total cost, render as text)
without editing every existing class each time.
from abc import ABC, abstractmethod
class ShapeVisitor(ABC):
@abstractmethod
def visit_circle(self, circle: "Circle"): ...
@abstractmethod
def visit_rectangle(self, rectangle: "Rectangle"): ...
class Shape(ABC):
@abstractmethod
def accept(self, visitor: ShapeVisitor): ... # "double dispatch" entry point
class Circle(Shape):
def __init__(self, radius): self.radius = radius
def accept(self, visitor): return visitor.visit_circle(self) # calls back with ITS OWN concrete type
class Rectangle(Shape):
def __init__(self, w, h): self.width, self.height = w, h
def accept(self, visitor): return visitor.visit_rectangle(self)
class AreaVisitor(ShapeVisitor): # a NEW operation, zero changes to Circle/Rectangle
def visit_circle(self, circle): return 3.14159 * circle.radius ** 2
def visit_rectangle(self, rectangle): return rectangle.width * rectangle.height
class JsonExportVisitor(ShapeVisitor): # ANOTHER new operation, still zero changes to Shape classes
def visit_circle(self, circle): return f'{{"type": "circle", "radius": {circle.radius}}}'
def visit_rectangle(self, rectangle): return f'{{"type": "rectangle", "w": {rectangle.width}, "h": {rectangle.height}}}'
shapes = [Circle(5), Rectangle(3, 4)]
area_visitor = AreaVisitor()
for shape in shapes:
print(shape.accept(area_visitor)) # 78.53975 then 12 — NEW behavior, Circle/Rectangle never edited
The honest tradeoff, named explicitly (this is a real, famous
tension in OOP called the expression problem): Visitor makes adding
new operations easy (a new visitor class, zero changes to the
hierarchy) but makes adding a new element type to the hierarchy hard
(every existing visitor must be updated with a new visit_X method,
or it won't compile/won't handle the new type). This is the exact
opposite tradeoff of ordinary polymorphism (module 02), where adding a
new subclass is easy but adding a new operation means touching every
subclass. Choose Visitor specifically when the hierarchy is stable and
operations are what keeps growing — not the reverse.
Hands-on exercises
1. Iterator
Implement a custom Playlist (reusing the idea from module 01's
exercises) that implements the iteration protocol (__iter__ /
IEnumerable<string>) so it can be used directly in a for/foreach
loop. Then add a second iteration method that only yields songs longer
than 3 minutes (using yield in both languages).
2. Chain of Responsibility
Build a support-ticket escalation chain: Level1Support handles
"simple" tickets, Level2Support handles "technical" tickets,
Level3Support (the terminal handler) handles everything else. Send
several tickets of different categories through the chain starting at
Level1Support, and confirm each lands with the right handler.
3. Mediator
Build an AirTrafficControl mediator through which Airplane objects
request permission to land — the mediator should only allow one plane
to land at a time (queue the rest), without any Airplane holding a
reference to any other Airplane.
4. Memento
Extend the Editor/History example to support redo as well as
undo — you'll need a second stack, and to think carefully about when
each stack gets pushed/cleared (hint: a new edit after an undo should
usually clear the redo stack).
5. Visitor
Add a third visitor, PerimeterVisitor, to the Circle/Rectangle
hierarchy from the Visitor section, computing each shape's perimeter.
Confirm you didn't need to touch Circle or Rectangle at all to add
it.
Independent challenge
No code given.
Task: Design a small document editor core, combining three
patterns from this module: a Document holding an ordered collection of
Elements (paragraphs and images, say) that supports Iterator-style
iteration (implement the language's native iteration protocol, per this
module's Iterator section) including a filtered iteration that skips
empty paragraphs; at least two Visitors operating on that same
element hierarchy without modifying Element/its subclasses — a
WordCountVisitor and an HtmlExportVisitor; and Memento-based
undo for edits to the document (adding or removing an element), with a
History caretaker that never inspects a memento's actual contents.
Hint
Keep Element's hierarchy genuinely stable for this exercise (this is
exactly when Visitor is the right call, per this module's tradeoff
discussion) — paragraph and image are enough element types; resist
adding a third mid-exercise just to test something, since every visitor
would then need updating. The Document's memento should snapshot the
entire list of elements (e.g., a copy of the list) at each save point —
watch for the same shallow-vs-deep-copy question from module 06's
Prototype pattern: if Elements themselves are mutable, a shallow list
copy might not be enough to protect a snapshot from later mutation.
Common mistakes & troubleshooting
- Hand-rolling a custom Iterator class when the language's built-in
protocol already does the job. In both Python and C#, implementing
__iter__/IEnumerable<T>directly on your own collection type is the idiomatic approach — reach for a separate, bespoke Iterator class only when you have a genuinely unusual traversal need the built-in protocol can't express. - A Chain of Responsibility with no guaranteed terminal handler. If
the last handler in the chain doesn't unconditionally handle whatever
reaches it, an unrecognized request silently disappears with no error
— always design an explicit fallback (as
Directordoes above), or have the chain itself raise/throw if nothing handled the request. - A Mediator that absorbs so much logic it becomes its own SRP violation (module 04) — a "God Mediator" that knows every detail of every participant's behavior, not just how to route messages between them, has just recreated the tight coupling problem in a new, centralized location instead of actually solving it.
- A Memento whose contents are fully exposed (public, mutable fields). If the caretaker (or anyone else) can read and modify a memento's internals directly, you've lost the actual point of the pattern — encapsulation of the snapshot. Keep the memento's state accessible only to its originator (via a convention or actual access modifier, per module 01).
- Reaching for Visitor when the element hierarchy is still actively growing. Per the expression-problem tradeoff explained above, adding a new element type means updating every existing visitor — fine for a stable hierarchy with growing operations, painful and error-prone (easy to forget a visitor) for a hierarchy that's still evolving.
Checkpoint quiz
- Why do you rarely hand-write a separate "Iterator class" in either Python or C# for ordinary collections?
- In the Chain of Responsibility example, what specifically happens if
you remove
Directorand someone callslead.approve(50000)? - What's the key difference between Observer (module 08) and Mediator — both centralize communication through one object?
- Why does
History(the Memento caretaker) never read a memento's actual content? - What's "the expression problem," and which side of it does Visitor optimize for?
- If you added a
Triangleto theShapehierarchy in the Visitor example, what specifically would you need to update, and how many places?
Answers
- Because both languages provide a first-class, built-in iteration
protocol (
__iter__/__next__or generators in Python;IEnumerable<T>/yield returnin C#) — implementing that protocol directly on your own type is applying the Iterator pattern; a separate bespoke Iterator class is only needed for unusual traversal logic the built-in protocol can't express. Manager.approvecallsself._next.approve(amount)/Next.Approve(), but_next/NextisNone/nullsince nothing was chained afterManager— this hits theelsebranch and prints "No one could approve this" (in Python/C# as written) rather than crashing, but the request genuinely goes unhandled — exactly the silent-drop risk named in this module.- Observer is a one-directional broadcast: a subject notifies many observers, but observers don't communicate with each other through it. Mediator specifically coordinates two-way communication between peers that would otherwise need direct references to each other.
- Because the caretaker's whole job is to store and return opaque
snapshots without understanding or manipulating their contents — only
the originator (
Editor) knows how to create and consume a memento's actual state, keeping that state encapsulated. - The expression problem is the tension between adding new types to a hierarchy easily versus adding new operations over that hierarchy easily — ordinary polymorphism optimizes for easy new types (hard to add new operations without touching every class); Visitor optimizes for the opposite: easy new operations (new visitor classes), at the cost of needing to update every existing visitor when a new type is added.
- You'd need to add a
visit_triangle/VisitTrianglemethod to theShapeVisitor/IShapeVisitorinterface itself, and then implement it in every existing concrete visitor (AreaVisitor,JsonExportVisitor, and any others) — one interface change plus one update per existing visitor class, which is exactly the cost Visitor trades away in exchange for easy new operations.
Interview questions
- "When would you use Chain of Responsibility over a simple
if/elif/else chain?"
When the set of possible handlers is open-ended, configurable, or
needs to be assembled/reordered independently of the calling code
(e.g., middleware pipelines, approval hierarchies that vary by
organization) — a hard-coded
if/elifchain requires editing the same function for every new case (an OCP violation, module 04); Chain of Responsibility lets you add a new handler as a new class, inserted into the chain, with zero changes elsewhere. - "What's the difference between Observer and Mediator? They both seem to centralize communication." Observer is one-directional: a subject broadcasts state changes to subscribed observers, who don't talk back through it to each other. Mediator specifically exists to coordinate two-way communication between peers (like chat users, or air traffic control coordinating planes) who would otherwise need direct references to one another.
- "How does the Memento pattern preserve encapsulation, compared to just exposing an object's internal state directly for an undo stack?" The memento is an opaque object whose contents only the originator (the object being snapshotted) can create or read — the caretaker managing the history of mementos never inspects or manipulates their contents, so the object's internal representation stays hidden from everything except itself.
- "What's the tradeoff of the Visitor pattern?" It makes adding new operations over a class hierarchy easy (a new visitor, zero changes to existing classes) but makes adding a new class to that hierarchy expensive (every existing visitor needs a new method) — the "expression problem" tradeoff, opposite to ordinary polymorphism. Use it when the hierarchy is stable and operations are what keeps growing.
- "Is implementing
IEnumerable<T>/__iter__on your own class 'using the Iterator pattern,' or is that something different?" It genuinely is the Iterator pattern — both languages made it a first-class built-in protocol precisely because the pattern is so broadly useful; you're expected to use the built-in protocol on your own types rather than hand-rolling a separate custom Iterator class in ordinary code.
Further reading & sources
- Refactoring.Guru: Behavioral Patterns - clear diagrams and multi-language examples, including all five patterns in this module.
- Python: Iterator Types - official reference for
__iter__/__next__and generators. - Microsoft Learn: Iterators (C#) - official reference for
IEnumerable<T>andyield return. - The Expression Problem (Wikipedia) - the formal statement of the tradeoff Visitor sits on one side of.
- Gang of Four: Design Patterns - the original source of all five patterns in this module.
Next
10-concurrency-safe-design — with the full pattern catalog in hand, we cover designing for concurrency: a properly thread-safe Singleton, producer-consumer pipelines, immutability as a concurrency strategy, and where locks actually belong in a design.