Design Patterns: Command
7 min
Command encapsulates a request as an object. That object can be stored, passed around, queued, undone, or replayed ??without the caller knowing anything about what the request actually does.
Intent
Suppose a text editor needs to support Ctrl+Z undo and Ctrl+Y redo. Without a clear pattern, tracking state before and after every operation becomes a mess.
Command solves this by wrapping each operation in an object that knows both how to execute itself and how to reverse itself.
Structure
- Command: interface declaring
execute()andundo() - ConcreteCommand: implements the operation and its reversal
- Receiver: the object that actually carries out the work (
TextEditor) - Invoker: triggers commands and maintains history (
CommandHistory)
Practical Example: Text Editor Undo/Redo
TypeScript
interface Command {
execute(): void;
undo(): void;
}
// Receiver
class TextEditor {
private content = '';
getContent(): string { return this.content; }
insertText(text: string, position: number): void {
this.content = this.content.slice(0, position) + text + this.content.slice(position);
}
deleteText(position: number, length: number): void {
this.content = this.content.slice(0, position) + this.content.slice(position + length);
}
}
// ConcreteCommand: insert
class InsertCommand 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);
}
}
// ConcreteCommand: delete
class DeleteCommand implements Command {
private deletedText = '';
constructor(
private editor: TextEditor,
private position: number,
private length: number,
) {}
execute(): void {
this.deletedText = this.editor.getContent().slice(this.position, this.position + this.length);
this.editor.deleteText(this.position, this.length);
}
undo(): void {
this.editor.insertText(this.deletedText, this.position);
}
}
// Invoker: manages command history
class CommandHistory {
private history: Command[] = [];
private redoStack: Command[] = [];
execute(command: Command): void {
command.execute();
this.history.push(command);
this.redoStack = []; // new action clears redo stack
}
undo(): void {
const command = this.history.pop();
if (command) {
command.undo();
this.redoStack.push(command);
}
}
redo(): void {
const command = this.redoStack.pop();
if (command) {
command.execute();
this.history.push(command);
}
}
}
const editor = new TextEditor();
const history = new CommandHistory();
history.execute(new InsertCommand(editor, 'Hello', 0));
history.execute(new InsertCommand(editor, ' World', 5));
console.log(editor.getContent()); // 'Hello World'
history.undo();
console.log(editor.getContent()); // 'Hello'
history.redo();
console.log(editor.getContent()); // 'Hello World'When to Use
Good fits
- Undo/redo functionality
- Queuing, scheduling, or delaying operations
- Audit logging of every operation performed
Summary
Command's core idea: turn behavior into an object. Once a request is an object, it can be stored, queued, passed around, and undone naturally.