20 minlesson

Introduction to the Command Pattern

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:

typescript
1class Order {
2 constructor(public id: string, public items: string[]) {}
3}
4
5class OrderManager {
6 private orders: Order[] = [];
7
8 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 }
13
14 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 }
21
22 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:

  1. No Undo/Redo: Once an operation is executed, there's no way to reverse it
  2. No Audit Trail: We can't track what operations were performed
  3. No Queuing: Operations must be executed immediately
  4. No Transaction Support: Can't group multiple operations together
  5. 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.

typescript
1// Command interface
2interface Command {
3 execute(): void;
4 undo(): void;
5 getDescription(): string;
6}
7
8// Receiver - the object that performs the actual work
9class OrderSystem {
10 private orders: Map<string, Order> = new Map();
11
12 addOrder(order: Order): void {
13 this.orders.set(order.id, order);
14 }
15
16 removeOrder(id: string): Order | undefined {
17 const order = this.orders.get(id);
18 this.orders.delete(id);
19 return order;
20 }
21
22 getOrder(id: string): Order | undefined {
23 return this.orders.get(id);
24 }
25
26 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}
36
37// Concrete Command: Create Order
38class CreateOrderCommand implements Command {
39 private order: Order;
40
41 constructor(
42 private orderSystem: OrderSystem,
43 private orderId: string,
44 private items: string[]
45 ) {
46 this.order = new Order(orderId, items);
47 }
48
49 execute(): void {
50 this.orderSystem.addOrder(this.order);
51 console.log(`Order ${this.orderId} created with items: ${this.items.join(', ')}`);
52 }
53
54 undo(): void {
55 this.orderSystem.removeOrder(this.orderId);
56 console.log(`Order ${this.orderId} creation undone`);
57 }
58
59 getDescription(): string {
60 return `Create order ${this.orderId}`;
61 }
62}
63
64// Concrete Command: Cancel Order
65class CancelOrderCommand implements Command {
66 private cancelledOrder?: Order;
67
68 constructor(
69 private orderSystem: OrderSystem,
70 private orderId: string
71 ) {}
72
73 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 }
81
82 undo(): void {
83 if (this.cancelledOrder) {
84 this.orderSystem.addOrder(this.cancelledOrder);
85 console.log(`Order ${this.orderId} cancellation undone`);
86 }
87 }
88
89 getDescription(): string {
90 return `Cancel order ${this.orderId}`;
91 }
92}
93
94// Concrete Command: Modify Order
95class ModifyOrderCommand implements Command {
96 private oldItems?: string[];
97
98 constructor(
99 private orderSystem: OrderSystem,
100 private orderId: string,
101 private newItems: string[]
102 ) {}
103
104 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 }
112
113 undo(): void {
114 if (this.oldItems) {
115 this.orderSystem.updateOrderItems(this.orderId, this.oldItems);
116 console.log(`Order ${this.orderId} modification undone`);
117 }
118 }
119
120 getDescription(): string {
121 return `Modify order ${this.orderId}`;
122 }
123}

Pattern Structure

1┌─────────────┐
2│ Client │
3└──────┬──────┘
4 │ creates
5
6┌─────────────┐
7│ Command │◄──────────┐
8│ Interface │ │
9└─────────────┘ │
10 △ │
11 │ implements │
12 │ │
13┌─────────────┐ │
14│ Concrete │ │
15│ Command │───────────┘
16└──────┬──────┘ uses
17
18 │ calls methods on
19
20┌─────────────┐
21│ Receiver │
22│ (OrderSys) │
23└─────────────┘
24
25┌─────────────┐
26│ Invoker │
27│ (CommandMgr)│───► stores & executes commands
28└─────────────┘

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

typescript
1// Invoker - manages and executes commands
2class CommandManager {
3 private history: Command[] = [];
4 private currentIndex: number = -1;
5
6 executeCommand(command: Command): void {
7 command.execute();
8
9 // Remove any commands after current index (they're now invalidated)
10 this.history = this.history.slice(0, this.currentIndex + 1);
11
12 // Add new command
13 this.history.push(command);
14 this.currentIndex++;
15 }
16
17 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 }
27
28 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 }
38
39 getHistory(): string[] {
40 return this.history.map((cmd, idx) => {
41 const marker = idx === this.currentIndex ? '→' : ' ';
42 return `${marker} ${cmd.getDescription()}`;
43 });
44 }
45}
46
47// Client code
48const orderSystem = new OrderSystem();
49const commandManager = new CommandManager();
50
51// Execute commands
52const createCmd1 = new CreateOrderCommand(orderSystem, 'ORD-001', ['Widget A', 'Widget B']);
53commandManager.executeCommand(createCmd1);
54
55const createCmd2 = new CreateOrderCommand(orderSystem, 'ORD-002', ['Gadget X']);
56commandManager.executeCommand(createCmd2);
57
58const modifyCmd = new ModifyOrderCommand(orderSystem, 'ORD-001', ['Widget A', 'Widget B', 'Widget C']);
59commandManager.executeCommand(modifyCmd);
60
61const cancelCmd = new CancelOrderCommand(orderSystem, 'ORD-002');
62commandManager.executeCommand(cancelCmd);
63
64console.log('\nCommand History:');
65console.log(commandManager.getHistory().join('\n'));
66
67// Undo operations
68commandManager.undo(); // Undo cancel
69commandManager.undo(); // Undo modify
70
71console.log('\nAfter Undo:');
72console.log(commandManager.getHistory().join('\n'));
73
74// Redo operations
75commandManager.redo(); // Redo modify
76
77console.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

  1. Undo/Redo Support: Each command knows how to reverse itself
  2. Command History: Full audit trail of all operations
  3. Queuing: Commands can be queued for later execution
  4. Macro Commands: Combine multiple commands into one
  5. Decoupling: Sender doesn't need to know about receiver implementation
  6. 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.