Command Pattern: Encapsulated Operations
The Command pattern encapsulates a request as an object, letting you parameterize clients with different requests, queue operations, log changes, and support undoable actions. It's one of the most...
Key Insights
- The Command pattern transforms method calls into objects, enabling undo/redo, queuing, logging, and transaction support with minimal coupling between components.
- Every command should encapsulate all the information needed to execute an action, including the receiver, method to invoke, and required parameters.
- Use the Command pattern when you need operation history, deferred execution, or want to treat actions as first-class citizens—skip it for simple, one-off method calls.
What is the Command Pattern?
The Command pattern encapsulates a request as an object, letting you parameterize clients with different requests, queue operations, log changes, and support undoable actions. It’s one of the most practical behavioral patterns in the Gang of Four catalog.
Think of a restaurant. When you order food, the waiter doesn’t run to the kitchen shouting your request. Instead, they write it on an order slip—a physical object that captures what you want, travels to the kitchen, can be queued behind other orders, and serves as a record of the transaction. The waiter (invoker) doesn’t need to know how to cook. The chef (receiver) doesn’t need to interact with customers. The order slip (command) decouples them completely.
This decoupling is the core benefit. The invoker—whatever triggers the action—knows nothing about how the action is performed. It just knows how to execute commands.
Anatomy of the Command Pattern
Four components make up the pattern:
- Command: An interface declaring the execution method (and often an undo method)
- Concrete Command: Implements the Command interface, binding a receiver to specific actions
- Receiver: The object that performs the actual work
- Invoker: Triggers commands without knowing their concrete types
- Client: Creates concrete commands and configures them with receivers
Here’s the foundational interface:
interface Command {
execute(): void;
undo(): void;
}
// A null object for when no command is assigned
class NoOpCommand implements Command {
execute(): void {}
undo(): void {}
}
The undo() method isn’t strictly required by the pattern, but it’s so commonly needed that I include it by default. The NoOpCommand prevents null checks throughout your invoker code—a small touch that pays dividends.
Basic Implementation
Let’s build the classic example: a remote control for home automation. We have receivers (the devices), commands (the operations), and an invoker (the remote).
// Receivers - the objects that do the actual work
class Light {
constructor(private location: string) {}
on(): void {
console.log(`${this.location} light is ON`);
}
off(): void {
console.log(`${this.location} light is OFF`);
}
}
class CeilingFan {
private speed: number = 0;
high(): void {
this.speed = 3;
console.log('Ceiling fan is on HIGH');
}
off(): void {
this.speed = 0;
console.log('Ceiling fan is OFF');
}
getSpeed(): number {
return this.speed;
}
}
// Concrete Commands
class LightOnCommand implements Command {
constructor(private light: Light) {}
execute(): void {
this.light.on();
}
undo(): void {
this.light.off();
}
}
class LightOffCommand implements Command {
constructor(private light: Light) {}
execute(): void {
this.light.off();
}
undo(): void {
this.light.on();
}
}
// Invoker
class RemoteControl {
private slots: Command[] = [];
private lastCommand: Command = new NoOpCommand();
setCommand(slot: number, command: Command): void {
this.slots[slot] = command;
}
pressButton(slot: number): void {
const command = this.slots[slot] ?? new NoOpCommand();
command.execute();
this.lastCommand = command;
}
pressUndo(): void {
this.lastCommand.undo();
}
}
// Client code
const livingRoomLight = new Light('Living Room');
const remote = new RemoteControl();
remote.setCommand(0, new LightOnCommand(livingRoomLight));
remote.setCommand(1, new LightOffCommand(livingRoomLight));
remote.pressButton(0); // Living Room light is ON
remote.pressButton(1); // Living Room light is OFF
remote.pressUndo(); // Living Room light is ON
Notice how the RemoteControl knows nothing about lights, fans, or any other device. It only knows about commands. You can add support for any new device by creating new command classes—the remote never changes.
Undo/Redo Support
Single-level undo is trivial. Multi-level undo requires maintaining a history stack. Here’s a text editor implementation:
interface TextCommand extends Command {
execute(): string;
undo(): string;
}
class TextEditor {
private content: string = '';
private history: TextCommand[] = [];
private redoStack: TextCommand[] = [];
getContent(): string {
return this.content;
}
setContent(content: string): void {
this.content = content;
}
executeCommand(command: TextCommand): void {
this.content = command.execute();
this.history.push(command);
this.redoStack = []; // Clear redo stack on new action
}
undo(): void {
const command = this.history.pop();
if (command) {
this.content = command.undo();
this.redoStack.push(command);
}
}
redo(): void {
const command = this.redoStack.pop();
if (command) {
this.content = command.execute();
this.history.push(command);
}
}
}
class InsertTextCommand implements TextCommand {
private previousContent: string = '';
constructor(
private editor: TextEditor,
private position: number,
private text: string
) {}
execute(): string {
this.previousContent = this.editor.getContent();
const content = this.previousContent;
const newContent =
content.slice(0, this.position) +
this.text +
content.slice(this.position);
return newContent;
}
undo(): string {
return this.previousContent;
}
}
class DeleteTextCommand implements TextCommand {
private deletedText: string = '';
private previousContent: string = '';
constructor(
private editor: TextEditor,
private position: number,
private length: number
) {}
execute(): string {
this.previousContent = this.editor.getContent();
this.deletedText = this.previousContent.slice(
this.position,
this.position + this.length
);
return (
this.previousContent.slice(0, this.position) +
this.previousContent.slice(this.position + this.length)
);
}
undo(): string {
return this.previousContent;
}
}
// Usage
const editor = new TextEditor();
editor.executeCommand(new InsertTextCommand(editor, 0, 'Hello'));
console.log(editor.getContent()); // "Hello"
editor.executeCommand(new InsertTextCommand(editor, 5, ' World'));
console.log(editor.getContent()); // "Hello World"
editor.undo();
console.log(editor.getContent()); // "Hello"
editor.redo();
console.log(editor.getContent()); // "Hello World"
The key insight: each command stores whatever state it needs to reverse itself. For insert, that’s the previous content. For delete, it’s both the previous content and what was deleted. This makes commands self-contained units of change.
Advanced Patterns: Macro Commands and Queuing
Commands compose naturally. A macro command is simply a command that executes other commands:
class MacroCommand implements Command {
constructor(private commands: Command[]) {}
execute(): void {
this.commands.forEach(cmd => cmd.execute());
}
undo(): void {
// Undo in reverse order
[...this.commands].reverse().forEach(cmd => cmd.undo());
}
}
// "Movie mode" - one button does everything
class DimLightsCommand implements Command {
constructor(private light: Light, private previousLevel: number = 100) {}
execute(): void { console.log('Lights dimmed to 20%'); }
undo(): void { console.log(`Lights restored to ${this.previousLevel}%`); }
}
class LowerBlindsCommand implements Command {
execute(): void { console.log('Blinds lowered'); }
undo(): void { console.log('Blinds raised'); }
}
class TVOnCommand implements Command {
execute(): void { console.log('TV turned on, Netflix launched'); }
undo(): void { console.log('TV turned off'); }
}
const movieMode = new MacroCommand([
new DimLightsCommand(new Light('Living Room')),
new LowerBlindsCommand(),
new TVOnCommand()
]);
movieMode.execute();
// Lights dimmed to 20%
// Blinds lowered
// TV turned on, Netflix launched
Commands also enable deferred execution through queuing:
interface Job extends Command {
readonly id: string;
readonly priority: number;
}
class JobQueue {
private queue: Job[] = [];
private processing: boolean = false;
enqueue(job: Job): void {
this.queue.push(job);
this.queue.sort((a, b) => b.priority - a.priority);
console.log(`Job ${job.id} queued`);
}
async processAll(): Promise<void> {
if (this.processing) return;
this.processing = true;
while (this.queue.length > 0) {
const job = this.queue.shift()!;
console.log(`Processing job ${job.id}`);
try {
job.execute();
} catch (error) {
console.error(`Job ${job.id} failed:`, error);
// Could implement retry logic, dead letter queue, etc.
}
}
this.processing = false;
}
}
class EmailJob implements Job {
readonly id: string;
readonly priority: number;
constructor(
private to: string,
private subject: string,
priority: number = 1
) {
this.id = `email-${Date.now()}`;
this.priority = priority;
}
execute(): void {
console.log(`Sending email to ${this.to}: ${this.subject}`);
}
undo(): void {
console.log('Cannot unsend email (but could mark for recall)');
}
}
Real-World Applications
The Command pattern appears everywhere once you know to look for it:
GUI frameworks use commands for menu items, toolbar buttons, and keyboard shortcuts. The same “Save” command object handles the menu click, the toolbar button, and Ctrl+S.
Transaction systems wrap database operations in commands. If any command fails, you walk backward through the executed commands calling undo(). This is the essence of the Unit of Work pattern.
Event sourcing stores commands (events) as the source of truth. The current state is derived by replaying all commands. This gives you a complete audit trail and the ability to reconstruct state at any point in time.
Task schedulers like cron jobs, background workers, and message queue consumers all process command objects that encapsulate the work to be done.
Trade-offs and When to Use
Benefits:
- Complete decoupling between invoker and receiver
- Easy to add new commands without changing existing code
- Built-in support for undo, redo, and transaction rollback
- Commands can be serialized, logged, queued, and replayed
- Enables macro commands and composite operations
Drawbacks:
- Class proliferation—each operation becomes its own class
- Added indirection can obscure what’s actually happening
- Overkill for simple, one-off operations
- State management for undo can become complex
Use the Command pattern when:
- You need undo/redo functionality
- You want to queue, schedule, or log operations
- You need to support transactions with rollback
- You’re building a plugin system where third parties define operations
- You want to decouple UI elements from business logic
Skip it when:
- Operations are simple and don’t need history
- There’s no requirement for queuing or deferred execution
- The added abstraction doesn’t provide clear value
- You’re building a prototype or throwaway code
The Command pattern trades simplicity for power. When you need that power—undo support, operation logging, deferred execution—it’s invaluable. When you don’t, it’s unnecessary ceremony. Choose accordingly.