Behavioral patterns
Patterns for how objects share work, react to change and swap behavior.
What you will learn
- 01Behavioral patterns
- 02Strategy: swap the algorithm
- 03Observer: tell whoever cares
- 04State: behavior depends on the state
- 05Command: a request as an object
- 06Other patterns worth knowing
- 07Choosing
Behavioral patterns
Behavioral patterns are about communication and responsibility: who does what, who is told, and how behavior can change without rewriting the code around it. Creational patterns made objects, structural patterns assembled them, and these make them cooperate.
Strategy: swap the algorithm
A strategy puts each interchangeable algorithm behind one interface, and a context object holds whichever one is chosen. You have already used it: PricingRule is a strategy.
class ParkingLot {
constructor(private rule: PricingRule) {}
setRule(rule: PricingRule) { this.rule = rule; }
fee(minutes: number) { return this.rule.fee(minutes); }
}
A holiday rule can replace the normal one at runtime, and an if chain is never needed. Use it whenever one task has several ways of being done.
Observer: tell whoever cares
An observer setup has a subject that keeps a list of subscribers and notifies them when something happens. The subject does not know what the subscribers do.
type Listener = (e: { plate: string; at: number }) => void;
class Gate {
private listeners: Listener[] = [];
subscribe(l: Listener) { this.listeners.push(l); }
open(plate: string, at: number) { this.listeners.forEach((l) => l({ plate, at })); }
}
A display board, a billing service and an audit log can all subscribe without the gate changing. The risks are hidden control flow and listeners that fail or run slowly, so decide how errors and ordering are handled.
Swipe sideways to pan · drag a box to move it
Swipe sideways to pan · drag a box to move it
Swipe sideways to pan · drag a box to move it
State: behavior depends on the state
The state pattern turns a state machine into objects. Each state is a class that knows how to respond to events, and the object delegates to its current state.
interface TicketState { pay(t: Ticket): TicketState; exit(t: Ticket): TicketState }
class Issued implements TicketState {
pay() { return new Paid(); }
exit(): TicketState { throw new Error("pay first"); }
}
class Paid implements TicketState {
pay(): TicketState { throw new Error("already paid"); }
exit() { return new Exited(); }
}
Illegal moves are impossible to forget, since each state decides. It replaces the pile of boolean flags with one clear structure.
Command: a request as an object
A command wraps a request with everything needed to run it. Because it is an object, you can queue it, log it, retry it, or undo it.
interface Command { run(): void; undo(): void }
class AssignSlot implements Command {
constructor(private lot: Lot, private id: string, private slot: number) {}
run() { this.lot.assign(this.id, this.slot); }
undo() { this.lot.release(this.slot); }
}
Undo stacks, job queues and macro recording all grow from this.
Other patterns worth knowing
- Template method: a base class defines the outline of an algorithm and subclasses fill in steps. Simple, but it uses inheritance, so prefer strategy when you can.
- Iterator: walk a collection without exposing how it is stored. Most languages build this in.
- Chain of responsibility: pass a request along handlers until one deals with it, as in request filters or approval steps.
- Mediator: have objects talk through one coordinator instead of to each other, reducing coupling.
Choosing
- Several ways to do one thing: strategy.
- Many parties must learn of an event: observer.
- Behavior changes with phase: state.
- Requests must be stored, retried or undone: command.
Each adds classes and indirection. Reach for one when a concrete problem in your code matches its shape, not before.