Behavioral patterns manage how objects collaborate and how responsibility flows. Strategy, Observer, and Command turn tangled conditionals and callbacks into clean, extensible designs.
Why: when a task has several interchangeable algorithms (sort orders, pricing rules, payment methods), hard-coding them into conditionals makes the class rigid — the Strategy pattern extracts each into its own object behind a common interface and lets you swap them. When: use it to replace a growing if/switch over "which behavior" with pluggable strategies. Where: it is composition + polymorphism applied to behavior, and it directly serves open/closed.
interface ShippingStrategy { cost(weight: number): number }
class Standard implements ShippingStrategy { cost(w: number) { return w * 1 } }
class Express implements ShippingStrategy { cost(w: number) { return w * 3 } }
class Cart {
constructor(private shipping: ShippingStrategy) {}
setShipping(s: ShippingStrategy) { this.shipping = s } // swap at runtime
total(weight: number) { return this.shipping.cost(weight) }
}
// Add a new option by adding a class — no editing Cart. (Compare: SOLID/OCP.)Why: when many objects must react to a change in another (UI updates, notifications, cache invalidation), wiring them directly couples everything — the Observer pattern lets a subject notify a list of subscribers without knowing who they are. When: use it for event-driven, one-to-many updates where subscribers come and go. Where: it is the in-process cousin of the event-driven architecture that connects distributed systems.
type Observer = (event: string) => void
class Subject {
private observers: Observer[] = []
subscribe(o: Observer) { this.observers.push(o) }
private notify(event: string) { this.observers.forEach((o) => o(event)) }
placeOrder() {
// ...do the work...
this.notify('order.placed') // subscribers react; subject doesn't know them
}
}
// Add analytics, email, inventory listeners without touching Subject.Why: representing an action as an object (with its parameters) decouples the caller from the receiver and unlocks queuing, logging, retry, and undo — because a command can be stored, passed around, and executed later. When: use it for undo/redo, task queues, transactional operations, or when you need an audit log of actions. Where: this is the pattern behind job queues and, conceptually, event sourcing.
interface Command { execute(): void; undo(): void }
class AddItemCommand implements Command {
constructor(private cart: Cart, private item: Item) {}
execute() { this.cart.add(this.item) }
undo() { this.cart.remove(this.item) }
}
// A command is data: queue it, log it, retry it, or reverse it later.
const history: Command[] = []
function run(cmd: Command) { cmd.execute(); history.push(cmd) }
function undoLast() { history.pop()?.undo() }