Behavioral Design Patterns
Behavioral patterns are concerned with communication between objects, how objects interact, and how responsibilities are distributed among them. They characterize complex control flows that are difficult to follow at runtime and shift your focus from flow of control to the way objects are interconnected.
Observer Pattern
The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. It is the foundation of event-driven programming.
// Type-safe event emitter
type EventMap = Record;
type EventHandler = (data: T) => void;
class TypedEventEmitter {
private handlers = new Map>>();
on(event: K, handler: EventHandler): () => void {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)!.add(handler as EventHandler);
// Return unsubscribe function
return () => {
this.handlers.get(event)?.delete(handler as EventHandler);
};
}
emit(event: K, data: T[K]): void {
this.handlers.get(event)?.forEach(handler => handler(data));
}
}
// Define events for an order system
interface OrderEvents {
"order:created": { orderId: string; customerId: string; total: number };
"order:paid": { orderId: string; paymentId: string };
"order:shipped": { orderId: string; trackingNumber: string };
"order:cancelled": { orderId: string; reason: string };
}
const orderBus = new TypedEventEmitter();
// Subscribe to events — type-safe!
const unsubscribe = orderBus.on("order:created", (data) => {
console.log(`New order ${data.orderId} for customer ${data.customerId}`);
// data is typed as { orderId: string; customerId: string; total: number }
});
orderBus.on("order:shipped", (data) => {
sendShippingNotification(data.orderId, data.trackingNumber);
});
// Emit events
orderBus.emit("order:created", {
orderId: "ord_123",
customerId: "cust_456",
total: 99.99,
});
// Cleanup
unsubscribe();
Strategy Pattern
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Strategy lets the algorithm vary independently from the clients that use it. It eliminates conditional statements by delegating behavior to strategy objects.
// Shipping cost calculation strategies
interface ShippingStrategy {
calculate(weight: number, distance: number): number;
estimateDays(distance: number): number;
getName(): string;
}
class StandardShipping implements ShippingStrategy {
calculate(weight: number, distance: number): number {
return weight * 0.5 + distance * 0.01;
}
estimateDays(distance: number): number {
return Math.ceil(distance / 500) + 3;
}
getName(): string { return "Standard Shipping"; }
}
class ExpressShipping implements ShippingStrategy {
calculate(weight: number, distance: number): number {
return weight * 1.5 + distance * 0.03 + 10; // Premium fee
}
estimateDays(distance: number): number {
return Math.ceil(distance / 1000) + 1;
}
getName(): string { return "Express Shipping"; }
}
class FreeShipping implements ShippingStrategy {
calculate(): number { return 0; }
estimateDays(distance: number): number { return Math.ceil(distance / 300) + 5; }
getName(): string { return "Free Shipping"; }
}
// Context uses the strategy
class ShippingCalculator {
constructor(private strategy: ShippingStrategy) {}
setStrategy(strategy: ShippingStrategy): void {
this.strategy = strategy;
}
getQuote(weight: number, distance: number): ShippingQuote {
return {
method: this.strategy.getName(),
cost: this.strategy.calculate(weight, distance),
estimatedDays: this.strategy.estimateDays(distance),
};
}
}
// Strategy selection based on business rules
function selectShippingStrategy(orderTotal: number, customerTier: string): ShippingStrategy {
if (orderTotal > 100 || customerTier === "premium") return new FreeShipping();
if (customerTier === "express") return new ExpressShipping();
return new StandardShipping();
}
const calculator = new ShippingCalculator(selectShippingStrategy(150, "regular"));
const quote = calculator.getQuote(5, 1000); // Free shipping (order > $100)
Command Pattern
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 knows how to execute itself and optionally how to undo itself.
// Command interface with execute and undo
interface Command {
execute(): Promise;
undo(): Promise;
describe(): string;
}
// Concrete commands for a text editor
class InsertTextCommand implements Command {
constructor(
private readonly document: TextDocument,
private readonly position: number,
private readonly text: string
) {}
async execute(): Promise {
this.document.insert(this.position, this.text);
}
async undo(): Promise {
this.document.delete(this.position, this.text.length);
}
describe(): string {
return `Insert "${this.text}" at position ${this.position}`;
}
}
class DeleteTextCommand implements Command {
private deletedText: string = "";
constructor(
private readonly document: TextDocument,
private readonly position: number,
private readonly length: number
) {}
async execute(): Promise {
this.deletedText = this.document.getText(this.position, this.length);
this.document.delete(this.position, this.length);
}
async undo(): Promise {
this.document.insert(this.position, this.deletedText);
}
describe(): string {
return `Delete ${this.length} chars at position ${this.position}`;
}
}
// Command history with undo/redo
class CommandHistory {
private history: Command[] = [];
private undone: Command[] = [];
async execute(command: Command): Promise {
await command.execute();
this.history.push(command);
this.undone = []; // Clear redo stack on new command
}
async undo(): Promise {
const command = this.history.pop();
if (!command) throw new Error("Nothing to undo");
await command.undo();
this.undone.push(command);
}
async redo(): Promise {
const command = this.undone.pop();
if (!command) throw new Error("Nothing to redo");
await command.execute();
this.history.push(command);
}
getHistory(): string[] {
return this.history.map(cmd => cmd.describe());
}
}
// Usage
const doc = new TextDocument();
const history = new CommandHistory();
await history.execute(new InsertTextCommand(doc, 0, "Hello "));
await history.execute(new InsertTextCommand(doc, 6, "World"));
// doc: "Hello World"
await history.undo();
// doc: "Hello "
await history.redo();
// doc: "Hello World"
State Pattern
The State pattern allows an object to alter its behavior when its internal state changes. The object appears to change its class. Each state is represented by a separate class, and the context delegates behavior to the current state object.
// State interface
interface OrderState {
name: string;
confirm(order: OrderContext): void;
ship(order: OrderContext): void;
deliver(order: OrderContext): void;
cancel(order: OrderContext): void;
}
// Concrete states
class DraftState implements OrderState {
name = "draft";
confirm(order: OrderContext): void {
console.log("Order confirmed — processing payment");
order.setState(new ConfirmedState());
}
ship(): void { throw new Error("Cannot ship a draft order"); }
deliver(): void { throw new Error("Cannot deliver a draft order"); }
cancel(order: OrderContext): void {
console.log("Draft order cancelled");
order.setState(new CancelledState());
}
}
class ConfirmedState implements OrderState {
name = "confirmed";
confirm(): void { throw new Error("Order is already confirmed"); }
ship(order: OrderContext): void {
console.log("Order shipped");
order.setState(new ShippedState());
}
deliver(): void { throw new Error("Cannot deliver before shipping"); }
cancel(order: OrderContext): void {
console.log("Confirmed order cancelled — refund initiated");
order.setState(new CancelledState());
}
}
class ShippedState implements OrderState {
name = "shipped";
confirm(): void { throw new Error("Order is already confirmed"); }
ship(): void { throw new Error("Order is already shipped"); }
deliver(order: OrderContext): void {
console.log("Order delivered!");
order.setState(new DeliveredState());
}
cancel(): void { throw new Error("Cannot cancel a shipped order"); }
}
class DeliveredState implements OrderState {
name = "delivered";
confirm(): void { throw new Error("Order is already delivered"); }
ship(): void { throw new Error("Order is already delivered"); }
deliver(): void { throw new Error("Order is already delivered"); }
cancel(): void { throw new Error("Cannot cancel a delivered order"); }
}
class CancelledState implements OrderState {
name = "cancelled";
confirm(): void { throw new Error("Cannot modify cancelled order"); }
ship(): void { throw new Error("Cannot modify cancelled order"); }
deliver(): void { throw new Error("Cannot modify cancelled order"); }
cancel(): void { throw new Error("Order is already cancelled"); }
}
// Context
class OrderContext {
private state: OrderState = new DraftState();
setState(state: OrderState): void {
console.log(`State transition: ${this.state.name} -> ${state.name}`);
this.state = state;
}
getState(): string { return this.state.name; }
confirm(): void { this.state.confirm(this); }
ship(): void { this.state.ship(this); }
deliver(): void { this.state.deliver(this); }
cancel(): void { this.state.cancel(this); }
}
// Usage — the order changes behavior based on its state
const order = new OrderContext();
order.confirm(); // draft -> confirmed
order.ship(); // confirmed -> shipped
order.deliver(); // shipped -> delivered
// order.cancel() // Error! Cannot cancel delivered order
Behavioral Pattern Selection
- Observer: When multiple objects need to react to state changes — event systems, reactive UIs
- Strategy: When you need interchangeable algorithms — sorting, pricing, validation
- Command: When you need undo/redo, queuing, or logging of operations
- State: When object behavior depends on state and you have complex state transitions