Command Pattern
Encapsulate requests as objects for undo, queue, and log operations
Pattern Overview
🎭 The Command Pattern - Request Encapsulation
Encapsulates requests as objects, allowing you to parameterize clients with different requests, queue operations, and support undo!
- Core Problem Solved:
- Decouple sender of request from receiver
- Support undo/redo operations easily
- Queue and schedule operations for later execution
- Log and audit all operations performed
- Simple Command: Basic command interface with execute method
- Macro Command: Composite commands executing multiple operations
- Undoable Command: Commands supporting undo/redo functionality
- Real-World Applications:
- Text editor undo/redo functionality
- GUI button actions and menu operations
- Database transaction logging and rollback
- Task scheduling and background job processing
- Remote procedure calls and API requests
- Smart home device control systems
- Modern Usage Examples:
- Redux actions in React state management
- Event sourcing in microservices
- Command buses in CQRS architecture
- Async task queues in Node.js applications
Examples:
Text editor undo/redo
Input:
invoker.executeCommand(new InsertTextCommand(editor, 'Hello', 0))Output:
Text inserted, can be undone with invoker.undo()Smart home remote control
Input:
remote.pressButton('living-room-light')Output:
Living room light is ON (can be undone with remote.pressUndo())Background task processing
Input:
taskQueue.addTask(new EmailTask('1', 'user@example.com', 'Hello', 'Message'))Output:
Task queued and processed: Email sent to user@example.comConcepts
Request EncapsulationUndo/Redo OperationsOperation QueuingDecouplingAction Objects
Complexity Analysis
Time:O(1)
Space:O(n)
Implementation
text-editor-commands
Time: O(1) | Space: O(n)
// Command interface
interface Command {
execute(): void;
undo?(): void;
}
// Receiver - performs the actual work
class TextEditor {
private content: string = '';
insertText(text: string, position: number): void {
this.content = this.content.slice(0, position) +
text +
this.content.slice(position);
}
deleteText(position: number, length: number): string {
const deleted = this.content.slice(position, position + length);
this.content = this.content.slice(0, position) +
this.content.slice(position + length);
return deleted;
}
getContent(): string {
return this.content;
}
}
// Concrete commands
class InsertTextCommand implements Command {
constructor(
private editor: TextEditor,
private text: string,
private position: number
) {}
execute(): void {
this.editor.insertText(this.text, this.position);
}
undo(): void {
this.editor.deleteText(this.position, this.text.length);
}
}
class DeleteTextCommand implements Command {
private deletedText: string = '';
constructor(
private editor: TextEditor,
private position: number,
private length: number
) {}
execute(): void {
this.deletedText = this.editor.deleteText(this.position, this.length);
}
undo(): void {
this.editor.insertText(this.deletedText, this.position);
}
}
// Invoker with undo/redo support
class EditorInvoker {
private history: Command[] = [];
private currentPosition: number = -1;
executeCommand(command: Command): void {
// Clear redo history when executing new command
this.history = this.history.slice(0, this.currentPosition + 1);
command.execute();
this.history.push(command);
this.currentPosition++;
}
undo(): boolean {
if (this.currentPosition >= 0) {
const command = this.history[this.currentPosition];
if (command.undo) {
command.undo();
this.currentPosition--;
return true;
}
}
return false;
}
redo(): boolean {
if (this.currentPosition < this.history.length - 1) {
this.currentPosition++;
this.history[this.currentPosition].execute();
return true;
}
return false;
}
}
// Usage
const editor = new TextEditor();
const invoker = new EditorInvoker();
invoker.executeCommand(new InsertTextCommand(editor, 'Hello', 0));
invoker.executeCommand(new InsertTextCommand(editor, ' World', 5));
console.log(editor.getContent()); // "Hello World"
invoker.undo(); // Removes " World"
console.log(editor.getContent()); // "Hello"
invoker.redo(); // Adds " World" back
console.log(editor.getContent()); // "Hello World"