Introduction to the Command Pattern
The Command pattern is a behavioral design pattern that encapsulates a request as an object, thereby allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations.
The Problem
In traditional programming, we often call methods directly on objects. While this works for simple scenarios, it creates several challenges:
typescript1class Order {2 constructor(public id: string, public items: string[]) {}3}45class OrderManager {6 private orders: Order[] = [];78 createOrder(id: string, items: string[]): void {9 const order = new Order(id, items);10 this.orders.push(order);11 console.log(`Order ${id} created`);12 }1314 cancelOrder(id: string): void {15 const index = this.orders.findIndex(o => o.id === id);16 if (index !== -1) {17 this.orders.splice(index, 1);18 console.log(`Order ${id} cancelled`);19 }20 }2122 modifyOrder(id: string, newItems: string[]): void {23 const order = this.orders.find(o => o.id === id);24 if (order) {25 order.items = newItems;26 console.log(`Order ${id} modified`);27 }28 }29}
Problems with this approach:
- No Undo/Redo: Once an operation is executed, there's no way to reverse it
- No Audit Trail: We can't track what operations were performed
- No Queuing: Operations must be executed immediately
- No Transaction Support: Can't group multiple operations together
- Tight Coupling: The caller is tightly coupled to the OrderManager implementation
The Solution: Command Pattern
The Command pattern solves these problems by encapsulating each request as an object. This object contains all the information needed to execute the operation and, optionally, to undo it.
typescript1// Command interface2interface Command {3 execute(): void;4 undo(): void;5 getDescription(): string;6}78// Receiver - the object that performs the actual work9class OrderSystem {10 private orders: Map<string, Order> = new Map();1112 addOrder(order: Order): void {13 this.orders.set(order.id, order);14 }1516 removeOrder(id: string): Order | undefined {17 const order = this.orders.get(id);18 this.orders.delete(id);19 return order;20 }2122 getOrder(id: string): Order | undefined {23 return this.orders.get(id);24 }2526 updateOrderItems(id: string, items: string[]): string[] | undefined {27 const order = this.orders.get(id);28 if (order) {29 const oldItems = [...order.items];30 order.items = items;31 return oldItems;32 }33 return undefined;34 }35}3637// Concrete Command: Create Order38class CreateOrderCommand implements Command {39 private order: Order;4041 constructor(42 private orderSystem: OrderSystem,43 private orderId: string,44 private items: string[]45 ) {46 this.order = new Order(orderId, items);47 }4849 execute(): void {50 this.orderSystem.addOrder(this.order);51 console.log(`Order ${this.orderId} created with items: ${this.items.join(', ')}`);52 }5354 undo(): void {55 this.orderSystem.removeOrder(this.orderId);56 console.log(`Order ${this.orderId} creation undone`);57 }5859 getDescription(): string {60 return `Create order ${this.orderId}`;61 }62}6364// Concrete Command: Cancel Order65class CancelOrderCommand implements Command {66 private cancelledOrder?: Order;6768 constructor(69 private orderSystem: OrderSystem,70 private orderId: string71 ) {}7273 execute(): void {74 this.cancelledOrder = this.orderSystem.removeOrder(this.orderId);75 if (this.cancelledOrder) {76 console.log(`Order ${this.orderId} cancelled`);77 } else {78 console.log(`Order ${this.orderId} not found`);79 }80 }8182 undo(): void {83 if (this.cancelledOrder) {84 this.orderSystem.addOrder(this.cancelledOrder);85 console.log(`Order ${this.orderId} cancellation undone`);86 }87 }8889 getDescription(): string {90 return `Cancel order ${this.orderId}`;91 }92}9394// Concrete Command: Modify Order95class ModifyOrderCommand implements Command {96 private oldItems?: string[];9798 constructor(99 private orderSystem: OrderSystem,100 private orderId: string,101 private newItems: string[]102 ) {}103104 execute(): void {105 this.oldItems = this.orderSystem.updateOrderItems(this.orderId, this.newItems);106 if (this.oldItems) {107 console.log(`Order ${this.orderId} modified. New items: ${this.newItems.join(', ')}`);108 } else {109 console.log(`Order ${this.orderId} not found`);110 }111 }112113 undo(): void {114 if (this.oldItems) {115 this.orderSystem.updateOrderItems(this.orderId, this.oldItems);116 console.log(`Order ${this.orderId} modification undone`);117 }118 }119120 getDescription(): string {121 return `Modify order ${this.orderId}`;122 }123}
Pattern Structure
1┌─────────────┐2│ Client │3└──────┬──────┘4 │ creates5 ▼6┌─────────────┐7│ Command │◄──────────┐8│ Interface │ │9└─────────────┘ │10 △ │11 │ implements │12 │ │13┌─────────────┐ │14│ Concrete │ │15│ Command │───────────┘16└──────┬──────┘ uses17 │18 │ calls methods on19 ▼20┌─────────────┐21│ Receiver │22│ (OrderSys) │23└─────────────┘2425┌─────────────┐26│ Invoker │27│ (CommandMgr)│───► stores & executes commands28└─────────────┘
Key Components:
- Command: Interface declaring execute() and undo()
- ConcreteCommand: Implements Command, stores receiver and parameters
- Receiver: The object that performs the actual work (OrderSystem)
- Invoker: Asks the command to execute (CommandManager)
- Client: Creates commands and sets their receiver
Using the Command Pattern
typescript1// Invoker - manages and executes commands2class CommandManager {3 private history: Command[] = [];4 private currentIndex: number = -1;56 executeCommand(command: Command): void {7 command.execute();89 // Remove any commands after current index (they're now invalidated)10 this.history = this.history.slice(0, this.currentIndex + 1);1112 // Add new command13 this.history.push(command);14 this.currentIndex++;15 }1617 undo(): void {18 if (this.currentIndex >= 0) {19 const command = this.history[this.currentIndex];20 command.undo();21 this.currentIndex--;22 console.log(`Undid: ${command.getDescription()}`);23 } else {24 console.log('Nothing to undo');25 }26 }2728 redo(): void {29 if (this.currentIndex < this.history.length - 1) {30 this.currentIndex++;31 const command = this.history[this.currentIndex];32 command.execute();33 console.log(`Redid: ${command.getDescription()}`);34 } else {35 console.log('Nothing to redo');36 }37 }3839 getHistory(): string[] {40 return this.history.map((cmd, idx) => {41 const marker = idx === this.currentIndex ? '→' : ' ';42 return `${marker} ${cmd.getDescription()}`;43 });44 }45}4647// Client code48const orderSystem = new OrderSystem();49const commandManager = new CommandManager();5051// Execute commands52const createCmd1 = new CreateOrderCommand(orderSystem, 'ORD-001', ['Widget A', 'Widget B']);53commandManager.executeCommand(createCmd1);5455const createCmd2 = new CreateOrderCommand(orderSystem, 'ORD-002', ['Gadget X']);56commandManager.executeCommand(createCmd2);5758const modifyCmd = new ModifyOrderCommand(orderSystem, 'ORD-001', ['Widget A', 'Widget B', 'Widget C']);59commandManager.executeCommand(modifyCmd);6061const cancelCmd = new CancelOrderCommand(orderSystem, 'ORD-002');62commandManager.executeCommand(cancelCmd);6364console.log('\nCommand History:');65console.log(commandManager.getHistory().join('\n'));6667// Undo operations68commandManager.undo(); // Undo cancel69commandManager.undo(); // Undo modify7071console.log('\nAfter Undo:');72console.log(commandManager.getHistory().join('\n'));7374// Redo operations75commandManager.redo(); // Redo modify7677console.log('\nAfter Redo:');78console.log(commandManager.getHistory().join('\n'));
Logistics Domain Context
In logistics and supply chain management, the Command pattern is particularly valuable:
Order Management
- CreateOrder: Create new shipping orders
- CancelOrder: Cancel orders with proper cleanup
- ModifyOrder: Update order items, quantities, or destinations
- SplitOrder: Split an order into multiple shipments
Inventory Operations
- AdjustInventory: Increase/decrease stock levels
- TransferStock: Move inventory between warehouses
- ReserveInventory: Reserve items for orders
- ReleaseInventory: Release reserved items
Shipment Commands
- SchedulePickup: Schedule carrier pickup
- AssignDriver: Assign driver to delivery
- UpdateLocation: Update shipment tracking
- CompleteDelivery: Mark delivery as complete
Benefits
- Undo/Redo Support: Each command knows how to reverse itself
- Command History: Full audit trail of all operations
- Queuing: Commands can be queued for later execution
- Macro Commands: Combine multiple commands into one
- Decoupling: Sender doesn't need to know about receiver implementation
- Transaction Support: Group commands into atomic transactions
When to Use
Use the Command pattern when you need:
- Undo/Redo functionality: Text editors, drawing apps, order management
- Operation logging: Audit trails, crash recovery
- Deferred execution: Job queues, schedulers
- Transaction rollback: Database operations, multi-step processes
- Macro operations: Combining multiple operations into one
Trade-offs
Pros:
- Supports undo/redo naturally
- Decouples invoker from receiver
- Easy to add new commands (Open/Closed Principle)
- Commands can be logged, queued, or composed
Cons:
- Increases number of classes (one per operation)
- May be overkill for simple operations
- Storing state for undo can increase memory usage
Key Takeaways
- Command pattern encapsulates requests as objects
- Enables undo/redo by storing operation state
- Commands contain everything needed to execute and reverse an operation
- Particularly useful in logistics for order management and inventory operations
- Creates audit trails and supports transactional operations
In the next lesson, we'll dive deeper into implementing the Command pattern with a focus on command queues and macro commands.