Module 08: Behavioral Patterns I
Why this matters
Creational patterns (module 06) cover making objects; structural
patterns (module 07) cover composing them. Behavioral patterns — split
across this module and module 09 — cover how objects communicate and
distribute responsibility between each other: who decides what to do,
who gets told when something happens, and how a request itself can be
treated as a first-class thing you can queue, log, or undo. You've
already met Strategy informally, twice (module 04's OCP fix, module 05's
Duck/FlyBehavior) — this module gives it, and four siblings, their
full formal treatment.
Concepts
Strategy — swap an algorithm at runtime, without an if chain
You've built this pattern twice already without naming it. Here's the canonical shape, made explicit: a Context holds a reference to a Strategy interface and delegates to it, and the caller decides which concrete strategy to plug in — including swapping it at runtime.
from abc import ABC, abstractmethod
class SortStrategy(ABC):
@abstractmethod
def sort(self, data: list) -> list: ...
class QuickSort(SortStrategy):
def sort(self, data):
print("sorting with quicksort")
return sorted(data) # stand-in for a real quicksort implementation
class BubbleSort(SortStrategy):
def sort(self, data):
print("sorting with bubble sort")
return sorted(data) # stand-in for a real bubble sort implementation
class Sorter: # the CONTEXT
def __init__(self, strategy: SortStrategy):
self.strategy = strategy
def set_strategy(self, strategy: SortStrategy): # swap the algorithm AT RUNTIME
self.strategy = strategy
def sort(self, data):
return self.strategy.sort(data)
sorter = Sorter(QuickSort())
sorter.sort([3, 1, 2])
sorter.set_strategy(BubbleSort()) # same Sorter object, different algorithm, no if/elif anywhere
sorter.sort([3, 1, 2])
Observer — notify many dependents automatically, when one thing changes
Problem it solves: one object's state change needs to automatically propagate to an open-ended, possibly-changing set of interested parties, without that object needing to know anything concrete about them.
from abc import ABC, abstractmethod
class Observer(ABC):
@abstractmethod
def update(self, temperature: float) -> None: ...
class WeatherStation: # the SUBJECT (publisher)
def __init__(self):
self._observers: list[Observer] = []
self._temperature = 0.0
def subscribe(self, observer: Observer):
self._observers.append(observer)
def unsubscribe(self, observer: Observer):
self._observers.remove(observer)
def set_temperature(self, temp: float):
self._temperature = temp
self._notify_all() # one state change, fanned out to everyone
def _notify_all(self):
for observer in self._observers:
observer.update(self._temperature)
class CurrentConditionsDisplay(Observer):
def update(self, temperature):
print(f"Current conditions: {temperature}°")
class StatisticsDisplay(Observer):
def __init__(self):
self.readings = []
def update(self, temperature):
self.readings.append(temperature)
print(f"Average so far: {sum(self.readings)/len(self.readings):.1f}°")
station = WeatherStation()
station.subscribe(CurrentConditionsDisplay())
station.subscribe(StatisticsDisplay())
station.set_temperature(72) # BOTH displays react — WeatherStation knows nothing about their details
station.set_temperature(76)
Command — turn a request into an object
Problem it solves: you want to decouple who requests an action from who performs it, and treat the request itself as a first-class object you can queue, log, or undo.
from abc import ABC, abstractmethod
class Command(ABC):
@abstractmethod
def execute(self): ...
@abstractmethod
def undo(self): ...
class Light: # the RECEIVER — does the actual work
def on(self): print("Light is ON")
def off(self): print("Light is OFF")
class LightOnCommand(Command):
def __init__(self, light: Light):
self.light = light
def execute(self):
self.light.on()
def undo(self):
self.light.off()
class RemoteControl: # the INVOKER — knows nothing about Light directly
def __init__(self):
self._history: list[Command] = []
def press_button(self, command: Command):
command.execute()
self._history.append(command)
def press_undo(self):
if self._history:
self._history.pop().undo()
remote = RemoteControl()
remote.press_button(LightOnCommand(Light())) # Light is ON
remote.press_undo() # Light is OFF — undo, without RemoteControl knowing HOW
State — behavior changes as an object's internal state changes
Problem it solves: an object's behavior needs to genuinely differ depending on which state it's in — not just "is this transition valid" (module 03's enum + transition-table already handles that cheaply), but "this method should actually do something different in each state."
from abc import ABC, abstractmethod
class OrderState(ABC):
@abstractmethod
def next(self, order: "Order"): ...
@abstractmethod
def name(self) -> str: ...
class PendingState(OrderState):
def next(self, order):
print("Shipping the order...")
order.state = ShippedState()
def name(self): return "Pending"
class ShippedState(OrderState):
def next(self, order):
print("Delivering the order...")
order.state = DeliveredState()
def name(self): return "Shipped"
class DeliveredState(OrderState):
def next(self, order):
print("Already delivered — nothing to do.")
def name(self): return "Delivered"
class Order:
def __init__(self):
self.state: OrderState = PendingState() # the ORDER delegates behavior to its current state object
def advance(self):
self.state.next(self) # Order itself has NO if/elif on status at all
order = Order()
print(order.state.name()) # Pending
order.advance() # Shipping the order...
order.advance() # Delivering the order...
print(order.state.name()) # Delivered
State vs. Strategy — the pattern almost everyone confuses, because the
code shape is nearly identical. Both hold a reference to an
interchangeable object and delegate to it. The difference is entirely
about intent and who changes the reference: in Strategy, the
client/caller picks the strategy from outside, for how to do
something (an algorithm choice), and it rarely changes itself mid-flow.
In State, the state object itself decides to transition the reference
to a new state (order.state = ShippedState(), from inside the state's
own method) in response to something happening — modeling what the
object currently is, changing autonomously over the object's lifetime.
State vs. module 03's enum-driven transitions — when to use which. If every state only needs a validity check (can we go from A to B?) with otherwise identical behavior, module 03's enum + transition dictionary is simpler and sufficient — reaching for full State-pattern classes there would be over-engineering (YAGNI, module 05). Use the full State pattern when different states need to genuinely behave differently, not just permit different next steps.
Template Method — fix the algorithm's shape, let subclasses fill in steps
Problem it solves: several variants of a process share the same overall sequence of steps, but differ in how one or two individual steps are done.
from abc import ABC, abstractmethod
class DataProcessor(ABC):
def process(self): # the TEMPLATE METHOD — fixes the sequence, never overridden
data = self.read_data()
result = self.transform_data(data)
self.save_data(result)
@abstractmethod
def read_data(self): ...
@abstractmethod
def transform_data(self, data): ...
def save_data(self, result): # a step with a sensible DEFAULT — subclasses may override if needed
print(f"Saving: {result}")
class CsvDataProcessor(DataProcessor):
def read_data(self):
return "a,b,c"
def transform_data(self, data):
return data.split(",")
class JsonDataProcessor(DataProcessor):
def read_data(self):
return '{"a": 1}'
def transform_data(self, data):
return {"parsed": data}
CsvDataProcessor().process() # Saving: ['a', 'b', 'c']
JsonDataProcessor().process() # Saving: {'parsed': '{"a": 1}'}
# the SEQUENCE (read -> transform -> save) is identical for both — only two steps actually differ
Template Method is the one pattern in this pair that uses inheritance directly rather than composition — and that's fine here specifically because the varying steps are genuinely part of one shared algorithm's identity, not an independently-swappable behavior (contrast with Strategy, which would be the composition-based alternative if you needed to swap the entire algorithm at runtime instead of overriding a couple of its steps once per subclass).
Hands-on exercises
1. Strategy
Build a PricingContext that holds a pricing Strategy
(RegularPricing, MemberPricing, ClearancePricing), each computing a
final price differently from a base price. Confirm swapping the strategy
on the same context object changes the computed price with no other code
changes.
2. Observer
Extend the WeatherStation example with a third observer,
ForecastDisplay, that prints a naive forecast based on whether the
temperature went up or down since the last reading. Then demonstrate
unsubscribe actually stops that observer from receiving further
updates.
3. Command
Extend the RemoteControl/Light example with LightOffCommand and a
second appliance, Fan, with its own on/off commands. Press several
buttons, then call undo three times in a row and confirm each undo
reverses the most recent action, in reverse order (LIFO).
4. State
Implement the Order/OrderState example, then add a CancelledState
reachable only from PendingState (not from ShippedState or
DeliveredState — attempting to cancel a shipped order should print a
clear rejection message instead of transitioning).
5. Template Method
Build a GameAI template method take_turn()/TakeTurn() that always
follows gather_info() → decide_move() → execute_move(), with two
concrete subclasses, AggressiveAI and DefensiveAI, that override
decide_move/DecideMove differently while sharing identical
gather_info/execute_move steps from the base class.
Independent challenge
No code given.
Task: Design a small smart home automation system combining
three patterns from this module: a Command for every device action
(TurnOnLightCommand, LockDoorCommand, etc.) issued through one
SmartHomeHub invoker that supports undo; an Observer-based
ActivityDashboard that gets notified of every command executed
(regardless of which device or command type) and maintains a running
log; and a Strategy for scheduling — implement at least two
scheduling strategies (ImmediateSchedule runs a command right away,
DelayedSchedule — simulate the delay, no real timers needed — logs
"would run after N seconds" instead of actually waiting) that the hub
can be configured with, swappable at runtime.
Hint
The Command objects should be the thing both the hub and the dashboard
know about — the hub executes them (and pushes them onto an undo stack,
exactly like RemoteControl), and after each execution it should notify
the dashboard (Observer) with some description of what just ran (the
command's own class name or a describe() method is enough — the
dashboard shouldn't need to know each command's internals). The
scheduling Strategy sits in front of command execution — the hub asks
its current strategy "how/when should this run?" before actually calling
execute(), and swapping strategies changes that timing behavior
without changing anything about the commands or the dashboard.
Common mistakes & troubleshooting
- Confusing Strategy and State because the code looks identical. Ask: who changes the reference, and why? If the client picks it once from outside for how to do something, it's Strategy. If the object itself changes it in response to something happening, representing what it currently is, it's State.
- The "lapsed listener" problem: forgetting to unsubscribe an
Observer. An observer that's no longer needed but never
unsubscribed keeps getting notified forever, and the subject keeps a
reference to it — preventing it from ever being cleaned up. Always
pair
subscribewith a correspondingunsubscribeat the point the observer is no longer needed. - A Command's
undo()that doesn't actually restore prior state.LightOnCommand.undo()callinglight.off()only works because "on" and "off" are each other's exact opposite; a command that, say, increases a value needs to remember the previous value itself (captured atexecute()time) to undo correctly — don't assume undo is always "do the opposite command." - Reaching for the full State pattern for a simple validity check. If nothing about behavior differs between states — only which transitions are legal — module 03's enum + transition table is simpler and sufficient; the class-per-state machinery is overkill there (YAGNI again).
- A Template Method base class with too many overridable "hook" steps. If nearly every step is abstract/overridable and subclasses end up overriding almost all of them differently, the shared "template" isn't actually providing much shared structure anymore — that's often a sign Strategy (swap the whole algorithm) fits better than Template Method (override a couple of steps within one fixed algorithm).
Checkpoint quiz
- In the Strategy example, what exactly does
Sorter(the context) not need to know aboutQuickSortandBubbleSort? - What problem does Observer solve that a subject calling each dependent directly, by name, would not solve as well?
- In the Command example, what does
RemoteControlnever need to know aboutLight? - What's the precise difference in intent between State and Strategy, given that their code shapes look nearly identical?
- Why is Template Method one of the few behavioral patterns that leans on inheritance rather than composition?
- Give one concrete symptom that tells you a "State pattern" situation is actually simple enough for module 03's enum + transition table instead.
Answers
- It doesn't need to know how either algorithm actually sorts — only
that whatever
Strategyit's holding implementssort()/Sort(); it can't even tell which concrete strategy is plugged in. - It lets the subject stay decoupled from a fixed, hard-coded list of dependents — new observer types can subscribe/unsubscribe at runtime without the subject's code ever changing, unlike a subject that calls out to specific named dependents directly.
- It never knows
Lighthason()/off()methods at all — it only knows it was handed something implementingCommand'sexecute()/undo(); the actual appliance and its API are entirely hidden inside the concrete command classes. - Strategy: the client chooses the behavior from outside, for how to do something (an algorithm), and the choice is comparatively stable. State: the object itself changes the reference from inside, in response to events, to reflect what it currently is — an ongoing, self-managed lifecycle.
- Because the varying pieces (individual steps) are genuinely part of
one shared algorithm's identity and structure, not independently
swappable behavior — inheritance is appropriate here specifically
because LSP is easy to honor: every subclass really is "a
DataProcessor, using the fixed process, with different step details." - If every state only ever needs a validity check (is this transition allowed?) and no state actually causes visibly different behavior beyond that — no method's actual logic differs by state, only which next states are legal — that's the enum + transition-table's exact use case, and the extra class-per-state machinery buys nothing.
Interview questions
- "What's the difference between Strategy and State? I've heard they look almost the same in code." They do — both hold and delegate to an interchangeable object behind a shared interface. The difference is intent and who drives the change: Strategy is picked by the calling code, from outside, for how to perform an operation, and stays fairly stable. State is changed by the object itself, from inside, as part of its own lifecycle, to represent what it currently is and change its own behavior accordingly.
- "How does Observer help with loose coupling?"
The subject (publisher) only depends on a small shared
Observerinterface, never on concrete observer classes — new observer types can be added, subscribed, and unsubscribed at runtime with zero changes to the subject's code, satisfying both OCP and DIP (module- at once.
- "How would you implement undo in a text editor using the Command
pattern?"
Every edit action (insert, delete, format) becomes a
Commandobject capturing whatever data it needs to reverse itself (e.g., the deleted text and its position, for a delete command's undo) — every executed command is pushed onto a history stack; undo pops the most recent command and calls itsundo(), which restores exactly what that specific command changed. - "When would you choose Template Method over Strategy?" When several variants share the same overall sequence of steps and only one or two individual steps genuinely differ — Template Method fixes that shared sequence once in a base class and lets subclasses override just the differing steps. Choose Strategy instead when you need to swap the entire algorithm interchangeably, especially at runtime, not just customize a couple of fixed steps within one shared structure.
- "What's a real-world risk with the Observer pattern in a long- running application?" The "lapsed listener" problem — an observer that's no longer needed but is never unsubscribed stays referenced by the subject forever, both continuing to receive unwanted notifications and preventing that observer object from being garbage-collected, a subtle memory leak.
Cumulative review
Closed-book. Pulls from modules 06–08.
- (06 + 08) Factory Method (module 06) and Strategy (module 08) both involve subclasses/implementations providing different behavior behind a shared abstract signature. What's the key difference in what is being varied?
- (07 + 08) The Decorator pattern (module 07) and the Command pattern (module 08) both wrap something behind a shared interface. What does each one add that the other doesn't?
- (06 + 08) Which module-06 pattern would you combine with Observer if you needed every newly-constructed object of a certain type to automatically register itself with a central tracker?
- (07 + 08) Explain why State (module 08) is a more behavior-driven cousin of Bridge (module 07) — what do they share structurally, and what's different about intent?
- (06 + 07 + 08) Name one YAGNI-flavored common mistake from each of modules 06, 07, and 08 that all boil down to the same underlying warning.
Answers
- Factory Method varies which concrete class gets constructed. Strategy varies which algorithm/behavior runs, on already- constructed, interchangeable objects — creation vs. runtime behavior-swapping.
- Decorator adds new behavior/responsibility to an object while preserving its interface. Command turns a request itself into an object, decoupling the sender from the receiver and enabling queuing/logging/undo — Decorator changes what an object does; Command changes what a request is.
- Singleton, specifically as a shared registry — a single
VehicleRegistry-style Singleton (as in module 06's independent challenge) that every new instance notifies (Observer-style, or simply by calling a registration method directly) upon construction. - Both compose an object with a reference to a separate abstraction it
delegates to (Bridge: a
Shapedelegates rendering to aRenderer; State: anOrderdelegates behavior to its currentOrderState). The difference: Bridge's implementation reference is chosen once, from outside, to let two axes vary independently by design; State's reference is changed from inside, autonomously, over the object's lifetime, to represent an evolving identity. - Module 06: building an Abstract Factory for only one product family. Module 07: building a Bridge, Proxy, or Flyweight before there's an actual second implementation/access-control need/memory problem. Module 08: reaching for the full State pattern for a simple validity check module 03's enum already handles. All three are the same warning: don't build a pattern's structural machinery ahead of a second, real, current requirement that justifies it.
Further reading & sources
- Refactoring.Guru: Behavioral Patterns - clear diagrams and multi-language examples, including the five patterns in this module.
- Martin Fowler: Event-driven notification and the Observer pattern - Observer's role beyond the textbook example, including its use in distributed systems.
- Gang of Four: Design Patterns - the original source of all five patterns in this module.
Next
09-behavioral-patterns-ii — Iterator, Chain of Responsibility, Mediator, Memento, and Visitor: the second half of the behavioral catalog, rounding out the 22 GoF patterns this track covers (of the 23 in the original catalog, only Interpreter — genuinely rare outside building parsers/DSLs — is skipped).