CMP-220 · Lesson 11 of 12 · 21 min

Behavioral design patterns

Structure tells you which objects exist; behavior tells you how they cooperate. The Hub's hardest behavioral problems are about change over time and about who needs to know what. When a node goes silent, the operations dashboard, the on-call technician, the customer's contract manager and the billing system all care, in different ways. A node moves through a lifecycle, from commissioning to active service, maintenance and retirement, and what it is allowed to do depends on where it is. Operators issue commands, such as re-aiming a node, that cannot be delivered until the node next connects, and that must be logged and sometimes undone. This lesson covers the behavioral patterns that solve these problems: strategy, observer, state and command, with shorter notes on iterator, chain of responsibility and visitor.

Strategy

The strategy pattern defines a family of algorithms, puts each in its own class behind a common interface, and makes them interchangeable, so the algorithm can vary independently of the code that uses it. You have already built one: the Classifier interface is a strategy, and ClassificationService is the context that holds one and delegates to it. The pattern's value is that choosing an algorithm becomes a matter of passing a different object, decided by configuration or even per request, instead of adding another branch to an if statement. In modern Java and Python, when a strategy has a single method, a lambda or a function is often the whole implementation: the Hub's interval rounding is a strategy chosen per customer, from rounding to the nearest 15 minutes to aligning with a city's signal-timing plan, and each is a one-line lambda stored in a map.

Strategy and the template method of lesson 7 solve a similar problem in opposite ways. Template method varies part of an algorithm by inheritance, fixed when the subclass is written. Strategy varies the whole algorithm by composition, and can change at run time. Following the advice of lesson 10, prefer strategy unless the steps genuinely share a lot of code and state.

Observer

The observer pattern defines a one-to-many dependency: when one object, the subject, changes state, all its registered observers are notified automatically. The subject knows only that its observers implement a small interface; it does not know what they do. In the Hub, the SilenceMonitor is a subject that publishes NodeWentSilent and NodeRecovered events. The dashboard observer turns a tile red, the paging observer sends a text message to the on-call technician if the node is on a priority corridor, the contract observer starts the clock on the customer's data-availability guarantee, and the billing observer marks the hours as not billable. Adding the contract observer last spring required no change to the monitor at all, which is the open-closed principle at work again.

A UML diagram of the observer pattern. A SilenceMonitor class, the subject, has methods subscribe, unsubscribe and publish, and an aggregation line to a NodeEventListener interface labeled listeners with multiplicity zero or more. The interface declares onSilent and onRecovered. Four classes realize it with dashed lines: DashboardTile, OnCallPager, ContractClock and BillingAdjuster. A note shows publish looping over listeners and calling onSilent on each.
The observer pattern: the monitor notifies every registered listener through a small interface and knows nothing about what each one does.

Observer is everywhere in software: graphical user interfaces deliver button clicks to listener objects, spreadsheets recalculate cells when their inputs change, and publish-subscribe messaging systems apply the same idea across networks. Three practical issues come with it. First, a slow or failing observer must not block the others; the Hub catches exceptions from each listener and logs them, and runs slow listeners such as paging on a separate thread. Second, observers that register and never unregister are a classic memory leak, the listener form of the cache leak in lesson 3. Third, observers should not depend on the order in which they are notified, which is usually unspecified.

State

The state pattern lets an object change its behavior when its internal state changes, as if it had changed class. Each state is represented by an object implementing a common interface, and the context object delegates to its current state object and replaces it when a transition occurs. The alternative, a field holding a status code and a switch statement in every method, scatters the rules for each state across the whole class, exactly the problem polymorphism solved in lesson 6. A node in the Hub is Commissioning when installed but not yet verified, Active when counting normally, Silent when it has missed uploads, InMaintenance when a technician has taken it offline, and Retired at the end of its life. Uploads are accepted and billed only when Active; an upload from a Silent node moves it back to Active and closes the alert; an upload from a node InMaintenance is stored but flagged as test data; and a Retired node's uploads are rejected.

A UML state machine diagram. A filled circle start marker points to Commissioning. Transitions are drawn as labeled arrows: Commissioning to Active on verified; Active to Silent on no upload for 30 minutes; Silent to Active on upload received; Active to InMaintenance and Silent to InMaintenance on technician starts work; InMaintenance to Active on technician finishes; Silent and InMaintenance to Retired on decommissioned; Retired leads to a final state marker. Inside each state box a short note says what uploads do: rejected, stored and billed, stored and alert closed, stored as test data, rejected.
The node lifecycle as a UML state machine. Each state handles uploads differently, and events trigger transitions between states.

UML state machine diagrams, like the one above, are a natural companion to the state pattern and to the finite state machines of CMP-210 and ELE-210 Digital Logic Design: states are rounded boxes, transitions are arrows labeled with the event that triggers them, and a filled circle marks the start. In code, each state class implements handlers such as onUpload and onTimeout, returning the next state. Java enums can implement the pattern compactly, because each enum constant can override methods, and a sealed interface with records works well too.

The state pattern with a sealed interface and records: each state decides what an upload means and which state comes next.
public sealed interface NodeState permits Commissioning, Active, Silent, InMaintenance, Retired {
    NodeState onUpload(NodeContext ctx, Upload u);
    default NodeState onTimeout(NodeContext ctx) { return this; }
}

public record Active() implements NodeState {
    public NodeState onUpload(NodeContext ctx, Upload u) { ctx.storeAndBill(u); return this; }
    public NodeState onTimeout(NodeContext ctx) { ctx.events().publishSilent(ctx.node()); return new Silent(); }
}

public record Silent() implements NodeState {
    public NodeState onUpload(NodeContext ctx, Upload u) {
        ctx.storeAndBill(u);
        ctx.events().publishRecovered(ctx.node());
        return new Active();
    }
}

public final class NodeContext {
    private NodeState state = new Commissioning();
    public void upload(Upload u) { state = state.onUpload(this, u); }     // delegate, then transition
    public void timeout()        { state = state.onTimeout(this); }
    // storeAndBill, events(), node() ...
}

Command

The command pattern turns a request into an object. Instead of calling node.reaim(lanes) directly, the operator console creates a ReaimCommand holding the node, the new lanes and the old lanes, and hands it to a command processor. Because the request is now an object, it can be queued, logged, sent across a network, scheduled for later, retried and undone. This fits the Hub exactly: a node can only receive configuration when it next connects, so commands wait in a per-node queue and are delivered at the next upload. Every command is written to an audit log with the operator's name and the time, which the city's contract requires. And because a ReaimCommand remembers the old lanes, an operator who aims a node at the wrong lanes can undo it.

from collections import deque

# Observer: the monitor notifies listeners without knowing what they do
class SilenceMonitor:
    def __init__(self): self._listeners = []
    def subscribe(self, listener): self._listeners.append(listener)
    def publish(self, event, node_id):
        for listener in self._listeners:
            try:
                listener(event, node_id)
            except Exception as e:                    # one bad listener must not stop the rest
                print(f"  listener failed: {e}")

log = []
monitor = SilenceMonitor()
monitor.subscribe(lambda ev, n: log.append(f"dashboard: node {n} {ev}"))
monitor.subscribe(lambda ev, n: log.append(f"pager: node {n} {ev}") if n in {4417} else None)
monitor.subscribe(lambda ev, n: 1 / 0)                # a buggy listener

# State: each state decides what an event means and which state follows
class Active:
    def upload(self, node): node.billed += 1; return self
    def timeout(self, node): monitor.publish("silent", node.node_id); return Silent()
class Silent:
    def upload(self, node):
        node.billed += 1; monitor.publish("recovered", node.node_id); return Active()
    def timeout(self, node): return self
class InMaintenance:
    def upload(self, node): node.test_uploads += 1; return self
    def timeout(self, node): return self              # no alerts during maintenance

class Node:
    def __init__(self, node_id):
        self.node_id, self.state, self.billed, self.test_uploads = node_id, Active(), 0, 0
        self.lanes, self.pending = (1, 2, 3, 4), deque()
    def upload(self):
        self.state = self.state.upload(self)
        while self.pending:                            # deliver queued commands at check-in
            self.pending.popleft().execute()
    def timeout(self): self.state = self.state.timeout(self)

# Command: a request as an object that can be queued, logged and undone
class ReaimCommand:
    def __init__(self, node, lanes, operator):
        self.node, self.new, self.old, self.operator = node, tuple(lanes), None, operator
    def execute(self):
        self.old, self.node.lanes = self.node.lanes, self.new
        print(f"  {self.operator}: node {self.node.node_id} lanes {self.old} -> {self.new}")
    def undo(self):
        self.node.lanes = self.old
        print(f"  undo: node {self.node.node_id} lanes back to {self.old}")

n = Node(4417)
n.upload(); n.timeout(); n.timeout(); n.upload()
print("state:", type(n.state).__name__, "| billed uploads:", n.billed)
print("\n".join(log))
cmd = ReaimCommand(n, [1, 2], "D. Whitcombe")
n.pending.append(cmd)                                  # queued until the node connects
print("queued; lanes still", n.lanes)
n.upload()
cmd.undo()
n.state = InMaintenance(); n.upload(); n.timeout()
print("maintenance uploads:", n.test_uploads, "| billed:", n.billed)
Output
  listener failed: division by zero
  listener failed: division by zero
state: Active | billed uploads: 2
dashboard: node 4417 silent
pager: node 4417 silent
dashboard: node 4417 recovered
pager: node 4417 recovered
queued; lanes still (1, 2, 3, 4)
  D. Whitcombe: node 4417 lanes (1, 2, 3, 4) -> (1, 2)
  undo: node 4417 lanes back to (1, 2, 3, 4)
maintenance uploads: 1 | billed: 3

Trace the output against the patterns. The node bills an upload while active, the first timeout makes it silent and notifies the listeners, the second timeout does nothing more because a silent node is already silent, and the next upload bills again and announces recovery. The buggy listener fails twice without stopping the dashboard or the pager. The re-aim command waits in the queue until the next upload, executes, and is undone with the old lanes it remembered. During maintenance, uploads are counted as test data and timeouts raise no alerts, all without a single if statement on a status code.

Three more patterns in brief

The iterator pattern, which provides a way to traverse a collection without exposing its structure, is so useful that languages have built it in: Java's Iterator and for-each loop and Python's iterators and generators from lesson 9 are the pattern. The chain of responsibility pattern passes a request along a chain of handlers until one deals with it, or, in a common variant, through every handler in turn. The Hub's data-quality pipeline is such a chain: a duplicate filter, a plausibility filter, a unit-consistency check and a clock-skew corrector each examine a detection, and adding a new check means inserting one more handler. The visitor pattern separates an operation from the classes it operates on, letting you add new operations over a fixed class hierarchy without editing it; Java 21's pattern matching over sealed types, shown in lesson 6, now covers many of the cases where visitor was once needed.

Behavioral patterns met in this course and where the Hub uses them.
PatternProblem it solvesExample in the Hub
StrategyChoose among interchangeable algorithms at run timeClassifiers, interval rounding rules
ObserverNotify many dependents of a change without coupling to themSilence and recovery alerts
StateChange behavior as an object moves through statesNode lifecycle
CommandQueue, log, schedule or undo requestsRe-aim and reboot commands, audit log
Template methodFix an algorithm's steps, vary some of themReport generation
Chain of responsibilityPass a request through a series of handlersData-quality pipeline
IteratorTraverse a collection without exposing its structureRecentBuffer, collections
VisitorAdd operations over a fixed hierarchyLargely replaced by sealed types and pattern matching

Worked problem

Choosing a pattern for alert escalation

A city wants alerts to escalate: a silent node first pages the technician on call; if nobody acknowledges within 20 minutes, it pages the supervisor; after 60 minutes, it emails the city's traffic operations center. Each customer may define a different escalation ladder. Which patterns fit, and how do they combine?

Show solution
  1. The trigger, a node going silent, is already published by the monitor; an EscalationListener subscribes as one more observer.
  2. Each alert moves through stages, Paged technician, Paged supervisor, Emailed city, Acknowledged, with time-outs as events: the state pattern models the alert's lifecycle.
  3. Each step in a ladder is an action to perform later, page this person or send this email, which can be scheduled, logged and canceled on acknowledgment: commands placed on a scheduler.
  4. The ladder itself differs per customer: a strategy object, built from configuration, that produces the commands for each stage.

Answer: Observer starts the process, state tracks each alert, commands carry out and record each step, and a strategy defines each customer's ladder. Real designs often combine several patterns, each solving one part of the problem.

Common mistakes

  • MistakeImplementing state-dependent behavior with a status field and switches in every method.

    InsteadUse the state pattern so each state's rules live together and transitions are explicit.

  • MistakeLetting one failing observer stop notification of the others.

    InsteadIsolate each listener: catch and log its exceptions, and run slow listeners asynchronously.

  • MistakeForgetting to unsubscribe observers.

    InsteadUnregister listeners when their owner goes away, or the subject keeps them alive forever.

  • MistakeExecuting commands directly when they need logging, queuing or undo.

    InsteadRepresent requests as command objects that remember what is needed to audit and reverse them.

  • MistakeReaching for a pattern name before understanding the problem.

    InsteadDescribe the forces first, then pick the simplest pattern, or none, that resolves them.

Summary and key terms

  • Strategy puts interchangeable algorithms behind an interface; lambdas often implement single-method strategies.
  • Observer lets a subject notify many listeners of changes without knowing what they do; isolate failures and unsubscribe.
  • State represents each state as an object that handles events and returns the next state, replacing switches on status codes.
  • Command turns requests into objects that can be queued, logged, scheduled and undone.
  • Iterator, chain of responsibility and visitor complete the common set; real designs combine several patterns.
Strategy pattern
Encapsulating interchangeable algorithms behind a common interface.
Observer pattern
A subject notifying registered observers automatically when it changes.
State pattern
Representing an object's states as objects so its behavior changes with its state.
Command pattern
Representing a request as an object that can be stored, queued, logged and undone.
Chain of responsibility
Passing a request along a series of handlers.
State machine diagram
A UML diagram of states and the events that cause transitions between them.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. How does strategy differ from template method?

    Show answer

    Strategy swaps a whole algorithm by composition, even at run time; template method varies steps of a fixed algorithm by inheritance.

  2. Why do observers that never unsubscribe cause memory leaks?

    Show answer

    The subject's listener list keeps them reachable, so they are never collected.

  3. In the node state machine, what happens to an upload from a node in maintenance?

    Show answer

    It is stored but flagged as test data, not billed, and the state does not change.

  4. Name three things the command pattern makes possible that a direct method call does not.

    Show answer

    Queuing for later delivery, audit logging and undo (also retry and scheduling).

  5. Which pattern fits a series of data-quality checks that each detection passes through?

    Show answer

    Chain of responsibility.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz