Loading learning content...
Objects have been created. Structures are in place. Now comes the most dynamic aspect of object-oriented design: how do objects work together? Individual objects, no matter how well-designed, accomplish little in isolation. Complex behavior emerges from the interactions between many objects—the patterns of communication, the distribution of responsibilities, and the orchestration of workflows.
Consider the coordination challenges in real systems:
These scenarios reveal that object interaction is as fundamental a design concern as creation or composition. Behavioral patterns provide proven solutions for distributing responsibilities and managing complex object collaborations.
This page explores Behavioral Design Patterns—the third and largest category in the Gang of Four's taxonomy. You'll understand how these patterns manage algorithms, responsibilities, and communication between objects. By the end, you'll recognize behavioral problems in your designs and know which patterns facilitate clean, extensible object collaboration.
Behavioral patterns are concerned with algorithms and the assignment of responsibilities between objects. They describe not just patterns of objects or classes, but the patterns of communication between them. These patterns characterize complex control flow that's difficult to follow at runtime.
The behavioral challenge:
As systems grow, the logic that governs how objects collaborate becomes increasingly complex. Without clear patterns:
Behavioral patterns bring order to these interactions by establishing clear protocols for how objects cooperate.
Two approaches in behavioral patterns:
Behavioral patterns use two primary mechanisms:
Inheritance-based — Define algorithm skeletons in base classes, let subclasses fill in details (Template Method)
Composition-based — Distribute behavior across multiple cooperating objects, achieving flexibility through delegation (Strategy, Observer, Command, etc.)
Most behavioral patterns favor composition, enabling behaviors to be combined and changed at runtime rather than fixed at compile time.
Creational patterns focus on what gets instantiated. Structural patterns focus on how things connect. Behavioral patterns focus on how things communicate and divide work. While the boundaries can blur (some patterns exhibit multiple characteristics), behavioral patterns distinctly address runtime dynamics—the flow of control and data between objects.
The Gang of Four identified eleven behavioral patterns—the largest category by far. This reflects the inherent complexity of modeling object interactions and the variety of communication challenges that arise in software systems.
| Pattern | Primary Problem Solved | Key Mechanism |
|---|---|---|
| Strategy | Interchangeable algorithms | Encapsulate algorithms in objects |
| Observer | One-to-many notifications | Subscribe/publish mechanism |
| Command | Encapsulate requests as objects | Command objects with execute() |
| State | Behavior varies by object state | State objects with behavior |
| Template Method | Algorithm skeleton with variation points | Abstract methods in base class |
| Iterator | Sequential access to collections | Iterator object with next/hasNext |
| Chain of Responsibility | Multiple potential handlers | Chain of handler objects |
| Mediator | Decouple many-to-many interactions | Central coordinator object |
| Memento | Capture and restore object state | Snapshot objects |
| Visitor | Add operations to class hierarchy | Double-dispatch via accept() |
| Interpreter | Interpret language/grammar | AST with interpret() method |
What unifies behavioral patterns:
Responsibility Assignment — Each pattern provides a clear answer to "who does what?"
Decoupled Communication — Objects interact through well-defined protocols, not direct dependencies
Runtime Flexibility — Behavior can often be changed, added, or composed at runtime
Algorithm Encapsulation — Logic is packaged into objects that can be passed, stored, and swapped
Let's explore the most essential behavioral patterns in depth.
The Problem:
You have a class that does something important, but the algorithm for doing it varies. You could use conditional statements (if/switch) to select algorithms, but this:
The Solution:
The Strategy pattern defines a family of algorithms, encapsulates each in its own class, and makes them interchangeable. The client can select the algorithm at runtime without changing the context class.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
// Strategy interfaceinterface ShippingStrategy { calculateCost(weight: number, distance: number): number; getEstimatedDays(): number; getName(): string;} // Concrete strategiesclass StandardShipping implements ShippingStrategy { calculateCost(weight: number, distance: number): number { return 0.05 * weight * distance; } getEstimatedDays(): number { return 5; } getName(): string { return "Standard Shipping"; }} class ExpressShipping implements ShippingStrategy { calculateCost(weight: number, distance: number): number { return 0.15 * weight * distance + 10; } getEstimatedDays(): number { return 2; } getName(): string { return "Express Shipping"; }} class OvernightShipping implements ShippingStrategy { calculateCost(weight: number, distance: number): number { return 0.30 * weight * distance + 25; } getEstimatedDays(): number { return 1; } getName(): string { return "Overnight Shipping"; }} class InternationalShipping implements ShippingStrategy { private region: "europe" | "asia" | "other"; constructor(region: "europe" | "asia" | "other") { this.region = region; } calculateCost(weight: number, distance: number): number { const regionMultiplier = { "europe": 1.5, "asia": 2.0, "other": 2.5 }; return 0.10 * weight * distance * regionMultiplier[this.region] + 30; } getEstimatedDays(): number { return this.region === "europe" ? 7 : 14; } getName(): string { return `International (${this.region})`; }} // Context class that uses a strategyclass Order { private items: { name: string; weight: number; price: number }[] = []; private shippingStrategy: ShippingStrategy; private deliveryDistance: number; constructor(distance: number, shippingStrategy?: ShippingStrategy) { this.deliveryDistance = distance; this.shippingStrategy = shippingStrategy || new StandardShipping(); } addItem(name: string, weight: number, price: number): void { this.items.push({ name, weight, price }); } // Strategy can be changed at runtime setShippingStrategy(strategy: ShippingStrategy): void { this.shippingStrategy = strategy; } private getTotalWeight(): number { return this.items.reduce((sum, item) => sum + item.weight, 0); } private getSubtotal(): number { return this.items.reduce((sum, item) => sum + item.price, 0); } // Context delegates to strategy getShippingCost(): number { return this.shippingStrategy.calculateCost( this.getTotalWeight(), this.deliveryDistance ); } getTotal(): number { return this.getSubtotal() + this.getShippingCost(); } getShippingSummary(): string { return `${this.shippingStrategy.getName()} - $${this.getShippingCost().toFixed(2)} (Est. ${this.shippingStrategy.getEstimatedDays()} days)`; }} // Client can compose different strategiesconst order = new Order(500); // 500 milesorder.addItem("Laptop", 5, 999);order.addItem("Mouse", 0.5, 49); // Show all optionsconst strategies = [ new StandardShipping(), new ExpressShipping(), new OvernightShipping(), new InternationalShipping("europe")]; strategies.forEach(strategy => { order.setShippingStrategy(strategy); console.log(order.getShippingSummary());}); // Output:// Standard Shipping - $137.50 (Est. 5 days)// Express Shipping - $422.50 (Est. 2 days)// Overnight Shipping - $850.00 (Est. 1 day)// International (europe) - $442.50 (Est. 7 days)The Problem:
You have an object (subject) whose state changes, and multiple other objects (observers) need to react to those changes. Direct calls from subject to observers create tight coupling—the subject must know about all observers, and adding new observers requires modifying the subject.
The Solution:
The Observer pattern defines a one-to-many dependency where the subject maintains a list of observers and notifies them automatically of any state changes. Observers subscribe to subjects and receive updates without the subject knowing their concrete types.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
// Observer interfaceinterface StockObserver { update(stock: string, price: number, change: number): void;} // Subject interfaceinterface StockSubject { subscribe(observer: StockObserver): void; unsubscribe(observer: StockObserver): void; notify(): void;} // Concrete subject - stock price feedclass StockTicker implements StockSubject { private observers: Set<StockObserver> = new Set(); private prices: Map<string, { current: number; previous: number }> = new Map(); subscribe(observer: StockObserver): void { this.observers.add(observer); console.log(`Observer subscribed. Total: ${this.observers.size}`); } unsubscribe(observer: StockObserver): void { this.observers.delete(observer); console.log(`Observer unsubscribed. Total: ${this.observers.size}`); } notify(): void { for (const [symbol, data] of this.prices) { const change = data.current - data.previous; this.observers.forEach(observer => { observer.update(symbol, data.current, change); }); } } // Business method that triggers notifications updatePrice(symbol: string, newPrice: number): void { const current = this.prices.get(symbol); this.prices.set(symbol, { current: newPrice, previous: current?.current ?? newPrice }); // Notify observers of change const change = newPrice - (current?.current ?? newPrice); this.observers.forEach(observer => { observer.update(symbol, newPrice, change); }); }} // Concrete observers with different behaviorsclass PriceDisplay implements StockObserver { private name: string; constructor(name: string) { this.name = name; } update(stock: string, price: number, change: number): void { const arrow = change >= 0 ? "▲" : "▼"; const color = change >= 0 ? "green" : "red"; console.log( `[${this.name}] ${stock}: $${price.toFixed(2)} ${arrow} (${change >= 0 ? "+" : ""}${change.toFixed(2)})` ); }} class PriceAlert implements StockObserver { private thresholds: Map<string, { low: number; high: number }> = new Map(); setAlert(symbol: string, low: number, high: number): void { this.thresholds.set(symbol, { low, high }); } update(stock: string, price: number, change: number): void { const threshold = this.thresholds.get(stock); if (!threshold) return; if (price <= threshold.low) { console.log(`🔔 ALERT: ${stock} dropped to $${price} (below $${threshold.low})`); } else if (price >= threshold.high) { console.log(`🔔 ALERT: ${stock} rose to $${price} (above $${threshold.high})`); } }} class TradingBot implements StockObserver { private portfolio: Map<string, number> = new Map(); private buyThreshold: number = -2; // Buy if drops 2% private sellThreshold: number = 5; // Sell if gains 5% update(stock: string, price: number, change: number): void { const percentChange = (change / (price - change)) * 100; if (percentChange <= this.buyThreshold) { console.log(`🤖 BOT: Buying ${stock} at $${price} (dip of ${percentChange.toFixed(1)}%)`); this.portfolio.set(stock, (this.portfolio.get(stock) || 0) + 1); } else if (percentChange >= this.sellThreshold) { const held = this.portfolio.get(stock) || 0; if (held > 0) { console.log(`🤖 BOT: Selling ${stock} at $${price} (gain of ${percentChange.toFixed(1)}%)`); this.portfolio.set(stock, held - 1); } } }} // Usage - loosely coupled observersconst ticker = new StockTicker(); const display = new PriceDisplay("Main Dashboard");const alert = new PriceAlert();alert.setAlert("AAPL", 150, 200);const bot = new TradingBot(); // Subscribe all observersticker.subscribe(display);ticker.subscribe(alert);ticker.subscribe(bot); // Subject changes trigger all observers automaticallyticker.updatePrice("AAPL", 175.50);ticker.updatePrice("AAPL", 168.25); // Drop triggers botticker.updatePrice("AAPL", 205.00); // High triggers alert // Unsubscribe selectivelyticker.unsubscribe(bot); // Bot stops receiving updatesObserver is foundational to reactive programming. You'll find it in: DOM event listeners (addEventListener), RxJS Observables, Vue's reactivity system, Redux store subscriptions, WebSocket message handlers, and pub/sub messaging systems. Understanding Observer helps you leverage these tools effectively.
The Problem:
You need to:
The Solution:
The Command pattern encapsulates a request as an object, allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations. Each command object knows the receiver and the action to perform.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
// Command interfaceinterface Command { execute(): void; undo(): void; getDescription(): string;} // Receiver - the actual text bufferclass TextBuffer { private content: string = ""; private cursorPosition: number = 0; getContent(): string { return this.content; } getCursor(): number { return this.cursorPosition; } insert(text: string, position: number): void { this.content = this.content.slice(0, position) + text + this.content.slice(position); this.cursorPosition = position + text.length; } delete(start: number, end: number): string { const deleted = this.content.slice(start, end); this.content = this.content.slice(0, start) + this.content.slice(end); this.cursorPosition = start; return deleted; } setCursor(position: number): void { this.cursorPosition = Math.max(0, Math.min(position, this.content.length)); }} // Concrete commandsclass InsertTextCommand implements Command { private buffer: TextBuffer; private text: string; private position: number; constructor(buffer: TextBuffer, text: string, position: number) { this.buffer = buffer; this.text = text; this.position = position; } execute(): void { this.buffer.insert(this.text, this.position); } undo(): void { this.buffer.delete(this.position, this.position + this.text.length); } getDescription(): string { return `Insert "${this.text}" at ${this.position}`; }} class DeleteTextCommand implements Command { private buffer: TextBuffer; private start: number; private end: number; private deletedText: string = ""; constructor(buffer: TextBuffer, start: number, end: number) { this.buffer = buffer; this.start = start; this.end = end; } execute(): void { this.deletedText = this.buffer.delete(this.start, this.end); } undo(): void { this.buffer.insert(this.deletedText, this.start); } getDescription(): string { return `Delete from ${this.start} to ${this.end}`; }} // Macro command - composite pattern for commandsclass MacroCommand implements Command { private commands: Command[] = []; private name: string; constructor(name: string) { this.name = name; } addCommand(command: Command): void { this.commands.push(command); } execute(): void { this.commands.forEach(cmd => cmd.execute()); } undo(): void { // Undo in reverse order [...this.commands].reverse().forEach(cmd => cmd.undo()); } getDescription(): string { return `Macro: ${this.name} (${this.commands.length} commands)`; }} // Invoker - manages command historyclass TextEditor { private buffer: TextBuffer = new TextBuffer(); private undoStack: Command[] = []; private redoStack: Command[] = []; executeCommand(command: Command): void { command.execute(); this.undoStack.push(command); this.redoStack = []; // Clear redo stack on new action console.log(`Executed: ${command.getDescription()}`); } undo(): void { const command = this.undoStack.pop(); if (command) { command.undo(); this.redoStack.push(command); console.log(`Undone: ${command.getDescription()}`); } else { console.log("Nothing to undo"); } } redo(): void { const command = this.redoStack.pop(); if (command) { command.execute(); this.undoStack.push(command); console.log(`Redone: ${command.getDescription()}`); } else { console.log("Nothing to redo"); } } // Convenience methods that create and execute commands type(text: string): void { const cmd = new InsertTextCommand( this.buffer, text, this.buffer.getCursor() ); this.executeCommand(cmd); } deleteSelection(start: number, end: number): void { const cmd = new DeleteTextCommand(this.buffer, start, end); this.executeCommand(cmd); } getContent(): string { return this.buffer.getContent(); } getHistory(): string[] { return this.undoStack.map(cmd => cmd.getDescription()); }} // Usageconst editor = new TextEditor(); editor.type("Hello");editor.type(" World");editor.type("!");console.log(editor.getContent()); // "Hello World!" editor.undo();console.log(editor.getContent()); // "Hello World" editor.undo();console.log(editor.getContent()); // "Hello" editor.redo();console.log(editor.getContent()); // "Hello World" // Command history is preservedconsole.log("History:", editor.getHistory());The Problem:
Your object's behavior varies dramatically based on its internal state. You find yourself with large methods full of conditional logic checking the current state before every action. Adding new states requires modifying many methods, and the state-transition logic is scattered everywhere.
The Solution:
The State pattern allows an object to alter its behavior when its internal state changes—the object appears to change its class. State-specific behavior is delegated to separate State objects, encapsulating what varies and making state transitions explicit.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
// State interfaceinterface OrderState { confirm(order: Order): void; ship(order: Order): void; deliver(order: Order): void; cancel(order: Order): void; getStatus(): string;} // Contextclass Order { private state: OrderState; private items: string[] = []; private history: { state: string; timestamp: Date }[] = []; constructor(items: string[]) { this.items = items; this.setState(new PendingState()); } setState(state: OrderState): void { const previousState = this.state?.getStatus() || "initial"; this.state = state; this.history.push({ state: state.getStatus(), timestamp: new Date() }); console.log(`Order transitioned: ${previousState} → ${state.getStatus()}`); } // Delegate all actions to current state confirm(): void { this.state.confirm(this); } ship(): void { this.state.ship(this); } deliver(): void { this.state.deliver(this); } cancel(): void { this.state.cancel(this); } getStatus(): string { return this.state.getStatus(); } getHistory(): typeof this.history { return this.history; }} // Concrete statesclass PendingState implements OrderState { confirm(order: Order): void { console.log("Payment processed. Order confirmed."); order.setState(new ConfirmedState()); } ship(order: Order): void { console.log("Cannot ship: order not yet confirmed."); } deliver(order: Order): void { console.log("Cannot deliver: order not yet shipped."); } cancel(order: Order): void { console.log("Order cancelled before confirmation."); order.setState(new CancelledState()); } getStatus(): string { return "PENDING"; }} class ConfirmedState implements OrderState { confirm(order: Order): void { console.log("Order already confirmed."); } ship(order: Order): void { console.log("Package handed to carrier. Shipment in progress."); order.setState(new ShippedState()); } deliver(order: Order): void { console.log("Cannot deliver: order not yet shipped."); } cancel(order: Order): void { console.log("Refund initiated. Order cancelled."); order.setState(new CancelledState()); } getStatus(): string { return "CONFIRMED"; }} class ShippedState implements OrderState { confirm(order: Order): void { console.log("Order already confirmed and shipped."); } ship(order: Order): void { console.log("Order already shipped."); } deliver(order: Order): void { console.log("Package delivered successfully!"); order.setState(new DeliveredState()); } cancel(order: Order): void { console.log("Cannot cancel: order already shipped. Request return instead."); } getStatus(): string { return "SHIPPED"; }} class DeliveredState implements OrderState { confirm(order: Order): void { console.log("Order completed. No further confirmation needed."); } ship(order: Order): void { console.log("Order already delivered."); } deliver(order: Order): void { console.log("Order already delivered."); } cancel(order: Order): void { console.log("Cannot cancel delivered order. Initiate return process."); } getStatus(): string { return "DELIVERED"; }} class CancelledState implements OrderState { confirm(order: Order): void { console.log("Cannot confirm: order is cancelled."); } ship(order: Order): void { console.log("Cannot ship: order is cancelled."); } deliver(order: Order): void { console.log("Cannot deliver: order is cancelled."); } cancel(order: Order): void { console.log("Order already cancelled."); } getStatus(): string { return "CANCELLED"; }} // Usage - clean, state-driven behaviorconst order = new Order(["Laptop", "Mouse"]); order.ship(); // Cannot ship: order not yet confirmed.order.confirm(); // Payment processed. Order confirmed.order.confirm(); // Order already confirmed.order.ship(); // Package handed to carrier. Shipment in progress.order.cancel(); // Cannot cancel: order already shipped.order.deliver(); // Package delivered successfully! console.log("\nOrder history:", order.getHistory());Both patterns encapsulate behavior behind interfaces, but their intent differs: Strategy is about choosing an algorithm at configuration time—the client selects the strategy. State is about an object changing behavior as its internal state changes—the state often transitions itself. Think of Strategy as 'how to do it' and State as 'what it can do now'.
The Problem:
You have an algorithm with a fixed structure, but some steps need to vary across different contexts. You want to define the algorithm's skeleton in a base class while allowing subclasses to customize specific steps without changing the overall structure.
The Solution:
The Template Method pattern defines the skeleton of an algorithm in a base class method, deferring some steps to subclasses. Subclasses can override specific steps without changing the algorithm's structure.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
// Abstract class with template methodabstract class DataMiner { // Template method - defines the algorithm skeleton // Marked final to prevent override (TypeScript doesn't have final, but conceptually) mine(path: string): AnalysisResult { console.log(`\nStarting data mining: ${path}`); // Fixed sequence of steps const rawData = this.openFile(path); const extracted = this.extractData(rawData); const parsed = this.parseData(extracted); const analyzed = this.analyzeData(parsed); const report = this.generateReport(analyzed); this.sendReport(report); this.cleanup(); console.log("Mining complete.\n"); return analyzed; } // Abstract methods - MUST be implemented by subclasses protected abstract openFile(path: string): RawData; protected abstract extractData(data: RawData): ExtractedData; protected abstract parseData(data: ExtractedData): ParsedData; // Hook methods - CAN be overridden, have default implementation protected analyzeData(data: ParsedData): AnalysisResult { console.log(" Performing standard analysis..."); return { recordCount: data.records.length, summary: "Standard analysis complete" }; } protected generateReport(result: AnalysisResult): Report { console.log(" Generating standard report..."); return { content: JSON.stringify(result), format: "json" }; } // Concrete methods - fixed implementation private sendReport(report: Report): void { console.log(` Sending report (format: ${report.format})...`); } private cleanup(): void { console.log(" Cleaning up resources..."); }} interface RawData { content: string; type: string; }interface ExtractedData { fields: string[]; rows: string[][]; }interface ParsedData { records: Record<string, unknown>[]; }interface AnalysisResult { recordCount: number; summary: string; }interface Report { content: string; format: string; } // Concrete implementationsclass CSVDataMiner extends DataMiner { protected openFile(path: string): RawData { console.log(` Opening CSV file: ${path}`); return { content: "name,age\nAlice,30\nBob,25", type: "csv" }; } protected extractData(data: RawData): ExtractedData { console.log(" Extracting CSV data..."); const lines = data.content.split("\n"); const fields = lines[0].split(","); const rows = lines.slice(1).map(line => line.split(",")); return { fields, rows }; } protected parseData(data: ExtractedData): ParsedData { console.log(" Parsing CSV records..."); const records = data.rows.map(row => { const record: Record<string, unknown> = {}; data.fields.forEach((field, i) => record[field] = row[i]); return record; }); return { records }; }} class JSONDataMiner extends DataMiner { protected openFile(path: string): RawData { console.log(` Opening JSON file: ${path}`); return { content: '[{"name":"Alice","age":30},{"name":"Bob","age":25}]', type: "json" }; } protected extractData(data: RawData): ExtractedData { console.log(" Extracting JSON data..."); return { fields: ["name", "age"], rows: [] }; // Different structure } protected parseData(data: ExtractedData): ParsedData { console.log(" Parsing JSON records..."); // In reality, would use the actual JSON return { records: [{ name: "Alice", age: 30 }, { name: "Bob", age: 25 }] }; } // Override hook to customize analysis protected analyzeData(data: ParsedData): AnalysisResult { console.log(" Performing JSON-specific deep analysis..."); const avgAge = data.records.reduce( (sum, r) => sum + (r.age as number), 0 ) / data.records.length; return { recordCount: data.records.length, summary: `JSON analysis: ${data.records.length} records, avg age: ${avgAge}` }; }} // Client uses the fixed template methodconst csvMiner = new CSVDataMiner();const jsonMiner = new JSONDataMiner(); csvMiner.mine("data/users.csv");jsonMiner.mine("data/users.json");The Hollywood Principle:
Template Method exemplifies the "Hollywood Principle": "Don't call us, we'll call you." The base class controls the flow and calls subclass methods at the right time—subclasses don't call base class methods to drive the flow. This inverts the typical call direction, centralizing control while distributing implementation.
The remaining behavioral patterns address specialized collaboration challenges:
Iterator Pattern:
Provides a way to access elements of a collection sequentially without exposing its underlying representation. Essential for abstracting traversal logic.
interface Iterator<T> {
next(): T | null;
hasNext(): boolean;
}
// Enables: for (const item of collection) { ... }
// Without knowing if collection is array, tree, graph, etc.
Chain of Responsibility Pattern:
Chains handlers that each decide whether to process a request or pass it along. Decouples senders from receivers.
abstract class Handler {
protected next: Handler | null = null;
setNext(handler: Handler): Handler {
this.next = handler;
return handler;
}
handle(request: Request): Response | null {
if (!this.canHandle(request) && this.next) {
return this.next.handle(request);
}
return this.process(request);
}
}
// Use cases: middleware pipelines, approval workflows, request filtering
Mediator Pattern:
Defines an object that encapsulates how a set of objects interact. Instead of objects referring to each other directly, they communicate through the mediator, reducing chaotic dependencies.
class ChatRoom /* Mediator */ {
private users: Map<string, User> = new Map();
send(message: string, from: User, to?: string): void {
if (to) {
this.users.get(to)?.receive(message, from.name);
} else {
this.users.forEach((user, name) => {
if (name !== from.name) user.receive(message, from.name);
});
}
}
}
// Users don't know about each other—only the mediator
Memento Pattern:
Captures and externalizes an object's internal state so it can be restored later. Essential for implementing undo, checkpoints, and rollbacks without violating encapsulation.
class EditorMemento {
private readonly content: string;
private readonly cursorPosition: number;
constructor(content: string, cursor: number) {
this.content = content;
this.cursorPosition = cursor;
}
getState(): { content: string; cursor: number } {
return { content: this.content, cursor: this.cursorPosition };
}
}
class Editor {
save(): EditorMemento { return new EditorMemento(this.content, this.cursor); }
restore(memento: EditorMemento): void { /* ... */ }
}
Master Strategy, Observer, Command, State, and Template Method first—they're the most commonly applied. Iterator is usually built into languages. Chain of Responsibility is essential for middleware/pipelines. Mediator helps manage complex UIs. Memento is critical for undo systems. Visitor and Interpreter are more specialized (AST processing, DSLs).
Behavioral patterns address the third fundamental challenge in object-oriented design: how do objects communicate and divide responsibilities? By establishing clear protocols for object collaboration, these patterns create systems where behavior is flexible, testable, and maintainable.
You now understand all three design pattern categories: Creational (managing object creation), Structural (managing object composition), and Behavioral (managing object interaction). Next, we'll explore why this categorization matters—how understanding the taxonomy accelerates pattern recognition and selection in real-world design challenges.