Loading learning content...
Having understood the problem—tight coupling between request invocation and execution—we're ready for the solution. The Command Pattern elegantly addresses these challenges by introducing a layer of indirection: command objects.
The Gang of Four defined the pattern's intent as:
"Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations."
This deceptively simple statement contains profound architectural implications. Let's unpack the pattern fully, examining its structure, participants, and implementation strategies.
By the end of this page, you will understand the complete Command Pattern structure: the Command interface, ConcreteCommand implementations, the Invoker, and the Receiver. You'll learn how to design command objects, implement the execute() method, and wire components together for fully decoupled request handling.
The Command Pattern involves four key participants, each with a distinct responsibility:
Declares the interface for executing an operation. At minimum, this includes an execute() method.
Implements the Command interface. Binds a Receiver to an action. Implements execute() by invoking the corresponding operation(s) on the Receiver.
Asks the command to execute the request. The Invoker doesn't know which ConcreteCommand it's holding—only that it has a Command with an execute() method.
Knows how to perform the actual work. Any class can be a Receiver. The Receiver has the domain knowledge to carry out the request.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
// ========================================// PARTICIPANT 1: Command Interface// ========================================// Declares the execution contract. All commands implement this.interface Command { execute(): void;} // ========================================// PARTICIPANT 2: ConcreteCommand// ========================================// Implements Command interface by delegating to a Receiver.// Encapsulates a receiver and the action to perform on it.class LightOnCommand implements Command { private receiver: Light; // Reference to the receiver constructor(receiver: Light) { this.receiver = receiver; } execute(): void { // Delegates to the receiver's actual operation this.receiver.turnOn(); }} class LightOffCommand implements Command { private receiver: Light; constructor(light: Light) { this.receiver = light; } execute(): void { this.receiver.turnOff(); }} // ========================================// PARTICIPANT 3: Receiver// ========================================// The object that knows how to perform the actual operations.// Commands delegate to receivers.class Light { private isOn: boolean = false; private location: string; constructor(location: string) { this.location = location; } turnOn(): void { this.isOn = true; console.log(`${this.location} light is now ON`); } turnOff(): void { this.isOn = false; console.log(`${this.location} light is now OFF`); }} // ========================================// PARTICIPANT 4: Invoker// ========================================// Holds a command and triggers its execution.// Doesn't know what the command does—only that it can execute.class RemoteControlButton { private command: Command; setCommand(command: Command): void { this.command = command; } press(): void { // The invoker doesn't know what will happen— // it just triggers the command this.command.execute(); }}Notice how the participants interact:
Invoker ↔ Command: The Invoker only knows the Command interface. It doesn't know about Light, Thermostat, or any receiver.
Command ↔ Receiver: The ConcreteCommand knows its specific Receiver. It binds the abstraction to the implementation.
Client (Assembler): Someone must create commands and configure invokers. This is typically done during setup/configuration.
The Button (Invoker) doesn't know about the Light (Receiver). It only knows about Commands. This means we can change what a button does without changing the button code. We can add new receivers without modifying the invoker. The coupling point shifts to the ConcreteCommand, which is designed to be simple and single-purpose.
The Command interface is the heart of the pattern. Its design affects everything else. Let's examine various interface designs and their implications.
123456789101112131415
// The simplest possible command interfaceinterface Command { execute(): void;} // This is sufficient for basic use cases:// - Decoupling invoker from receiver// - Queuing operations// - Logging operations // But it doesn't support:// - Undo// - Status reporting// - Async execution// - Validation123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
// Extended interface supporting undointerface UndoableCommand { execute(): void; undo(): void;} // Now commands must implement their own reversalclass InsertTextCommand implements UndoableCommand { private document: Document; private position: number; private text: string; constructor(document: Document, position: number, text: string) { this.document = document; this.position = position; this.text = text; } execute(): void { this.document.insertAt(this.position, this.text); } undo(): void { // Reverse of insert = delete the same text this.document.deleteRange( this.position, this.position + this.text.length ); }} class DeleteTextCommand implements UndoableCommand { private document: Document; private start: number; private end: number; private deletedText: string = ""; // Captured on execute constructor(document: Document, start: number, end: number) { this.document = document; this.start = start; this.end = end; } execute(): void { // Capture deleted text BEFORE deleting (for undo) this.deletedText = this.document.getRange(this.start, this.end); this.document.deleteRange(this.start, this.end); } undo(): void { // Reverse of delete = re-insert the captured text this.document.insertAt(this.start, this.deletedText); }}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
// Full-featured interface for complex applicationsinterface EnterpriseCommand { // Core execution execute(): Promise<CommandResult>; // Reversal undo(): Promise<CommandResult>; redo(): Promise<CommandResult>; // Inspection getName(): string; getDescription(): string; getParameters(): Record<string, unknown>; // Validation canExecute(): boolean; validate(): ValidationResult; // Lifecycle isExecuted(): boolean; isUndone(): boolean; // Serialization serialize(): string;} // Result type for tracking outcomesinterface CommandResult { success: boolean; error?: Error; data?: unknown; timestamp: Date;} // Validation resultinterface ValidationResult { valid: boolean; errors: string[]; warnings: string[];} // Abstract base class providing common functionalityabstract class BaseCommand implements EnterpriseCommand { protected executed: boolean = false; protected undone: boolean = false; protected executedAt?: Date; abstract execute(): Promise<CommandResult>; abstract undo(): Promise<CommandResult>; abstract getName(): string; abstract getDescription(): string; abstract getParameters(): Record<string, unknown>; async redo(): Promise<CommandResult> { if (!this.undone) { return { success: false, error: new Error("Cannot redo: not undone"), timestamp: new Date() }; } return this.execute(); } canExecute(): boolean { return this.validate().valid && !this.executed; } validate(): ValidationResult { return { valid: true, errors: [], warnings: [] }; } isExecuted(): boolean { return this.executed; } isUndone(): boolean { return this.undone; } serialize(): string { return JSON.stringify({ name: this.getName(), parameters: this.getParameters(), executed: this.executed, executedAt: this.executedAt, }); }}Start with the simplest interface that meets your needs. Over-engineering the Command interface creates unnecessary implementation burden. Add undo() only if you need undo. Add canExecute() only if you need validation. The minimal interface is often sufficient for decoupling alone.
ConcreteCommand classes are where the pattern comes alive. Each command encapsulates:
Let's explore several ConcreteCommand implementations:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
// ========================================// RECEIVER: Various smart home devices// ========================================class GarageDoor { private isOpen: boolean = false; open(): void { this.isOpen = true; console.log("Garage door is now OPEN"); } close(): void { this.isOpen = false; console.log("Garage door is now CLOSED"); } getState(): boolean { return this.isOpen; }} class Thermostat { private temperature: number = 70; setTemperature(temp: number): void { console.log(`Setting temperature from ${this.temperature}°F to ${temp}°F`); this.temperature = temp; } getTemperature(): number { return this.temperature; }} class Stereo { private volume: number = 0; private isPlaying: boolean = false; on(): void { console.log("Stereo is ON"); } off(): void { console.log("Stereo is OFF"); this.volume = 0; this.isPlaying = false; } setVolume(level: number): void { this.volume = level; console.log(`Stereo volume set to ${level}`); } playCD(): void { this.isPlaying = true; console.log("Playing CD..."); } getVolume(): number { return this.volume; }} // ========================================// COMMAND INTERFACE WITH UNDO// ========================================interface Command { execute(): void; undo(): void;} // ========================================// CONCRETE COMMANDS// ======================================== // Simple toggle commandclass GarageDoorOpenCommand implements Command { private garageDoor: GarageDoor; constructor(garageDoor: GarageDoor) { this.garageDoor = garageDoor; } execute(): void { this.garageDoor.open(); } undo(): void { this.garageDoor.close(); }} class GarageDoorCloseCommand implements Command { private garageDoor: GarageDoor; constructor(garageDoor: GarageDoor) { this.garageDoor = garageDoor; } execute(): void { this.garageDoor.close(); } undo(): void { this.garageDoor.open(); }} // Command with parameters and state captureclass ThermostatSetCommand implements Command { private thermostat: Thermostat; private targetTemperature: number; private previousTemperature: number = 0; // Captured for undo constructor(thermostat: Thermostat, temperature: number) { this.thermostat = thermostat; this.targetTemperature = temperature; } execute(): void { // Capture current state BEFORE changing (for undo) this.previousTemperature = this.thermostat.getTemperature(); this.thermostat.setTemperature(this.targetTemperature); } undo(): void { // Restore to captured previous state this.thermostat.setTemperature(this.previousTemperature); }} // Composite command (macro) invoking multiple receiver methodsclass StereoOnWithCDCommand implements Command { private stereo: Stereo; private previousVolume: number = 0; constructor(stereo: Stereo) { this.stereo = stereo; } execute(): void { // A single command can orchestrate multiple receiver operations this.stereo.on(); this.stereo.playCD(); this.previousVolume = this.stereo.getVolume(); this.stereo.setVolume(11); // Sets it to eleven! } undo(): void { this.stereo.setVolume(this.previousVolume); this.stereo.off(); }}Pattern 1: State Capture for Undo
Before modifying state in execute(), capture the current state. The undo() method restores this captured state.
Pattern 2: Multi-Step Commands A single command can invoke multiple receiver methods. This is how you create "macro" operations that compose multiple primitive operations.
Pattern 3: Parameter Encapsulation All parameters needed for execution are stored as instance variables in the command. This allows the command to be created long before execution.
For reliable undo, you must capture all state that execute() modifies. Missing state capture leads to incomplete or incorrect undo. Consider capturing state in execute() rather than the constructor—this ensures you capture the state at execution time, not at command creation time.
The Invoker is the object that triggers command execution. It holds a reference to a Command and calls execute() at the appropriate time. The Invoker knows nothing about the ConcreteCommand type or the Receiver—it only knows the Command interface.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
// ========================================// SIMPLE INVOKER: Single command, immediate execution// ========================================class SimpleButton { private command: Command | null = null; setCommand(command: Command): void { this.command = command; } press(): void { if (this.command) { this.command.execute(); } }} // ========================================// MULTI-SLOT INVOKER: Multiple commands// ========================================class RemoteControl { private onCommands: Command[] = []; private offCommands: Command[] = []; private undoCommand: Command | null = null; setCommand(slot: number, onCommand: Command, offCommand: Command): void { this.onCommands[slot] = onCommand; this.offCommands[slot] = offCommand; } pressOnButton(slot: number): void { if (this.onCommands[slot]) { this.onCommands[slot].execute(); this.undoCommand = this.onCommands[slot]; // Track for undo } } pressOffButton(slot: number): void { if (this.offCommands[slot]) { this.offCommands[slot].execute(); this.undoCommand = this.offCommands[slot]; } } pressUndoButton(): void { if (this.undoCommand) { this.undoCommand.undo(); } }} // ========================================// INVOKER WITH COMMAND HISTORY// ========================================class CommandInvokerWithHistory { private commandHistory: Command[] = []; private undoStack: Command[] = []; private redoStack: Command[] = []; executeCommand(command: Command): void { command.execute(); this.commandHistory.push(command); this.undoStack.push(command); this.redoStack = []; // Clear redo on new command } undo(): void { const command = this.undoStack.pop(); if (command) { command.undo(); this.redoStack.push(command); } } redo(): void { const command = this.redoStack.pop(); if (command) { command.execute(); this.undoStack.push(command); } } getHistory(): Command[] { return [...this.commandHistory]; }} // ========================================// INVOKER WITH DELAYED EXECUTION// ========================================class ScheduledInvoker { private scheduledCommands: Array<{ command: Command; executeAt: Date; }> = []; scheduleCommand(command: Command, delay: number): void { const executeAt = new Date(Date.now() + delay); this.scheduledCommands.push({ command, executeAt }); setTimeout(() => { command.execute(); this.removeFromSchedule(command); }, delay); } private removeFromSchedule(command: Command): void { const index = this.scheduledCommands .findIndex(s => s.command === command); if (index !== -1) { this.scheduledCommands.splice(index, 1); } } cancelScheduled(command: Command): void { this.removeFromSchedule(command); // In real implementation, would also cancel the timeout } getPendingCommands(): Array<{ command: Command; executeAt: Date }> { return [...this.scheduledCommands]; }}The Invoker can take many forms depending on your application:
| Invoker Type | Use Case | Characteristics |
|---|---|---|
| GUI Button | UI interactions | Single command, immediate execution on click |
| Menu System | Application menus | Multiple commands mapped to menu items |
| Keyboard Shortcut Manager | Hotkey handling | Commands mapped to key combinations |
| Command Queue | Background processing | FIFO execution, potentially async |
| Scheduler | Timed execution | Commands execute at specific times |
| Transaction Manager | Database operations | Batched execution with rollback |
| Macro Recorder | Automation | Records and replays command sequences |
Think of the Invoker as the component that connects user intent (button press, menu selection, API call) to domain action (the command). This separation is what enables features like undo history, command logging, and execution scheduling without changing either the UI or the domain logic.
A common challenge with Invokers is handling unassigned slots or optional commands. Rather than checking for null everywhere, we can use the Null Object Pattern applied to commands.
The Null Command (or NoOpCommand) is a command that does nothing. It satisfies the Command interface but has an empty execute() method. This eliminates null checks and provides consistent behavior.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
// ========================================// THE NULL COMMAND: Does nothing, safely// ========================================class NoOpCommand implements Command { execute(): void { // Intentionally empty } undo(): void { // Intentionally empty }} // ========================================// INVOKER USING NULL COMMANDS// ========================================class RemoteControlWithNullCommands { private slots: number = 7; private onCommands: Command[]; private offCommands: Command[]; private undoCommand: Command; constructor() { const noOp = new NoOpCommand(); // Initialize all slots with NoOpCommand this.onCommands = Array(this.slots).fill(noOp); this.offCommands = Array(this.slots).fill(noOp); this.undoCommand = noOp; } setCommand(slot: number, onCommand: Command, offCommand: Command): void { this.onCommands[slot] = onCommand; this.offCommands[slot] = offCommand; } pressOnButton(slot: number): void { // No null check needed—NoOpCommand handles empty slots this.onCommands[slot].execute(); this.undoCommand = this.onCommands[slot]; } pressOffButton(slot: number): void { // No null check needed this.offCommands[slot].execute(); this.undoCommand = this.offCommands[slot]; } pressUndoButton(): void { // No null check needed this.undoCommand.undo(); }} // ========================================// COMPARISON: With vs Without Null Command// ======================================== // WITHOUT Null Command (fragile, cluttered)class RemoteWithoutNullCommand { private onCommands: (Command | null)[] = []; pressOnButton(slot: number): void { const command = this.onCommands[slot]; if (command !== null && command !== undefined) { command.execute(); } // What if we forget this check? NullPointerException! }} // WITH Null Command (clean, safe)class RemoteWithNullCommand { private onCommands: Command[] = []; // All initialized to NoOpCommand pressOnButton(slot: number): void { this.onCommands[slot].execute(); // Always safe }}Now let's see a complete example that demonstrates all participants working together. This example models a home automation remote control with multiple devices and full undo support.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
// ========================================// COMMAND INTERFACE// ========================================interface Command { execute(): void; undo(): void; getName(): string;} // ========================================// NULL COMMAND// ========================================class NoOpCommand implements Command { execute(): void {} undo(): void {} getName(): string { return "No Operation"; }} // ========================================// RECEIVERS// ========================================class Light { private location: string; private isOn: boolean = false; private brightness: number = 100; constructor(location: string) { this.location = location; } on(): void { this.isOn = true; console.log(`[${this.location}] Light is ON`); } off(): void { this.isOn = false; console.log(`[${this.location}] Light is OFF`); } dim(level: number): void { this.brightness = level; console.log(`[${this.location}] Light dimmed to ${level}%`); } getBrightness(): number { return this.brightness; }} class CeilingFan { private location: string; private speed: "OFF" | "LOW" | "MEDIUM" | "HIGH" = "OFF"; constructor(location: string) { this.location = location; } setSpeed(speed: "OFF" | "LOW" | "MEDIUM" | "HIGH"): void { this.speed = speed; console.log(`[${this.location}] Ceiling fan set to ${speed}`); } getSpeed(): "OFF" | "LOW" | "MEDIUM" | "HIGH" { return this.speed; }} // ========================================// CONCRETE COMMANDS// ========================================class LightOnCommand implements Command { private light: Light; constructor(light: Light) { this.light = light; } execute(): void { this.light.on(); } undo(): void { this.light.off(); } getName(): string { return "Light On"; }} class LightOffCommand implements Command { private light: Light; constructor(light: Light) { this.light = light; } execute(): void { this.light.off(); } undo(): void { this.light.on(); } getName(): string { return "Light Off"; }} class LightDimCommand implements Command { private light: Light; private targetLevel: number; private previousLevel: number = 100; constructor(light: Light, level: number) { this.light = light; this.targetLevel = level; } execute(): void { this.previousLevel = this.light.getBrightness(); this.light.dim(this.targetLevel); } undo(): void { this.light.dim(this.previousLevel); } getName(): string { return `Light Dim to ${this.targetLevel}%`; }} class CeilingFanSpeedCommand implements Command { private fan: CeilingFan; private targetSpeed: "OFF" | "LOW" | "MEDIUM" | "HIGH"; private previousSpeed: "OFF" | "LOW" | "MEDIUM" | "HIGH" = "OFF"; constructor( fan: CeilingFan, speed: "OFF" | "LOW" | "MEDIUM" | "HIGH" ) { this.fan = fan; this.targetSpeed = speed; } execute(): void { this.previousSpeed = this.fan.getSpeed(); this.fan.setSpeed(this.targetSpeed); } undo(): void { this.fan.setSpeed(this.previousSpeed); } getName(): string { return `Fan Speed ${this.targetSpeed}`; }} // ========================================// INVOKER: Full-featured remote control// ========================================class HomeRemoteControl { private slots: number = 7; private onCommands: Command[]; private offCommands: Command[]; private undoStack: Command[] = []; private redoStack: Command[] = []; constructor() { const noOp = new NoOpCommand(); this.onCommands = Array(this.slots).fill(noOp); this.offCommands = Array(this.slots).fill(noOp); } setCommand( slot: number, onCommand: Command, offCommand: Command ): void { this.onCommands[slot] = onCommand; this.offCommands[slot] = offCommand; } pressOnButton(slot: number): void { const command = this.onCommands[slot]; command.execute(); this.undoStack.push(command); this.redoStack = []; // Clear redo on new action console.log(` → Executed: ${command.getName()}`); } pressOffButton(slot: number): void { const command = this.offCommands[slot]; command.execute(); this.undoStack.push(command); this.redoStack = []; console.log(` → Executed: ${command.getName()}`); } pressUndo(): void { const command = this.undoStack.pop(); if (command) { command.undo(); this.redoStack.push(command); console.log(` ↩ Undone: ${command.getName()}`); } else { console.log(" ⚠ Nothing to undo"); } } pressRedo(): void { const command = this.redoStack.pop(); if (command) { command.execute(); this.undoStack.push(command); console.log(` ↪ Redone: ${command.getName()}`); } else { console.log(" ⚠ Nothing to redo"); } } printStatus(): void { console.log("\n=== Remote Control Status ==="); for (let i = 0; i < this.slots; i++) { console.log( `Slot ${i}: ON[${this.onCommands[i].getName()}] OFF[${this.offCommands[i].getName()}]` ); } console.log(`Undo stack depth: ${this.undoStack.length}`); console.log(`Redo stack depth: ${this.redoStack.length}`); }}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152
// ========================================// CLIENT: Wires everything together// ========================================function demonstrateCommandPattern(): void { // Create receivers (devices) const livingRoomLight = new Light("Living Room"); const kitchenLight = new Light("Kitchen"); const bedroomFan = new CeilingFan("Bedroom"); // Create commands (bind receivers to actions) const livingRoomLightOn = new LightOnCommand(livingRoomLight); const livingRoomLightOff = new LightOffCommand(livingRoomLight); const kitchenLightOn = new LightOnCommand(kitchenLight); const kitchenLightOff = new LightOffCommand(kitchenLight); const kitchenLightDim = new LightDimCommand(kitchenLight, 50); const bedroomFanHigh = new CeilingFanSpeedCommand(bedroomFan, "HIGH"); const bedroomFanOff = new CeilingFanSpeedCommand(bedroomFan, "OFF"); // Create invoker (remote control) const remote = new HomeRemoteControl(); // Configure slots (connect invoker to commands) remote.setCommand(0, livingRoomLightOn, livingRoomLightOff); remote.setCommand(1, kitchenLightOn, kitchenLightOff); remote.setCommand(2, bedroomFanHigh, bedroomFanOff); // Print initial configuration remote.printStatus(); console.log("\n=== User Interactions ===\n"); // User presses buttons remote.pressOnButton(0); // Living room light ON remote.pressOnButton(1); // Kitchen light ON remote.pressOnButton(2); // Bedroom fan HIGH console.log("\n--- User decides to undo ---\n"); remote.pressUndo(); // Undo fan HIGH (back to OFF) remote.pressUndo(); // Undo kitchen light ON console.log("\n--- User redoes the fan ---\n"); remote.pressRedo(); // Redo fan HIGH remote.printStatus();} // Run the demonstrationdemonstrateCommandPattern();Notice how the remote (Invoker) knows nothing about lights, fans, or any receiver. It only knows Commands. We can add new devices, new commands, or change button assignments without modifying the remote. This is the architectural flexibility the Command Pattern provides.
In languages with first-class functions (JavaScript, TypeScript, Python, Java 8+), we can simplify simple commands using lambdas. When commands don't need undo or additional methods, a function reference suffices.
This is a valid simplification for some use cases, but it trades away capabilities like undo, serialization, and inspection.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
// ========================================// SIMPLIFIED COMMAND USING FUNCTIONS// ========================================type SimpleCommand = () => void; class SimplifiedRemote { private commands: Map<string, SimpleCommand> = new Map(); setCommand(name: string, command: SimpleCommand): void { this.commands.set(name, command); } press(name: string): void { const command = this.commands.get(name); if (command) { command(); } }} // Usageconst light = new Light("Living Room");const remote = new SimplifiedRemote(); // Lambda commandsremote.setCommand("light-on", () => light.on());remote.setCommand("light-off", () => light.off());remote.setCommand("light-dim-50", () => light.dim(50)); // Multi-step lambdaremote.setCommand("movie-mode", () => { light.dim(20); // tv.on(); // curtains.close();}); remote.press("light-on");remote.press("movie-mode"); // ========================================// WHEN TO USE LAMBDAS VS CLASSES// ========================================/*USE LAMBDA COMMANDS when:- No undo needed- No serialization needed- No inspection/introspection needed- Commands are simple, single operations- Commands don't need to store state USE CLASS COMMANDS when:- Undo/redo is required- Commands need to capture state- Commands need serialization (for logging, replay)- Commands need getName(), describe(), or other metadata- Commands are complex, multi-step operations- You need strong typing on command parameters*/ // ========================================// HYBRID APPROACH: Functions with undo// ========================================interface FunctionalCommand { execute: () => void; undo: () => void;} const createLightCommand = (light: Light): FunctionalCommand => { let wasOn = false; // Captured state return { execute: () => { wasOn = false; // Assuming it was off light.on(); }, undo: () => { if (!wasOn) { light.off(); } } };};Lambda commands are concise but limited. They lack the ability to store undo state, serialize for logging, or provide metadata. For simple one-shot invocations, lambdas work well. For anything requiring undo, queuing, or logging, prefer full Command classes.
We've explored the complete structure and implementation of the Command Pattern. Let's consolidate the key concepts:
execute()) and add methods (undo(), canExecute()) as neededWhat's Next:
In the next page, we'll dive deep into undo/redo support—the most compelling capability that command objectification enables. We'll explore undo stack management, complex undo scenarios, multi-level undo, and the challenges of implementing reliable state reversal.
You now understand the complete structure of the Command Pattern. You've seen how to design Command interfaces, implement ConcreteCommands, build Invokers, and wire everything together. The pattern's power lies in the decoupling: invokers don't know receivers, commands encapsulate operations, and the system gains flexibility for undo, logging, queuing, and more.