Learning Objectives
- Understand the Command pattern concept
- Encapsulate actions as objects
- Implement undo/redo functionality
- Create command queues
- Apply command best practices
What is the Command Pattern?
Command turns requests into stand-alone objects containing all information about the request. This enables undo/redo, queuing, logging, and decoupling of sender and receiver.
Basic Example
class Command {
execute() {
throw new Error('execute() must be implemented');
}
undo() {
throw new Error('undo() must be implemented');
}
}
class AddCommand extends Command {
constructor(receiver, value) {
super();
this.receiver = receiver;
this.value = value;
}
execute() {
this.receiver.add(this.value);
console.log(`Added ${this.value}`);
}
undo() {
this.receiver.subtract(this.value);
console.log(`Undid add of ${this.value}`);
}
}
class SubtractCommand extends Command {
constructor(receiver, value) {
super();
this.receiver = receiver;
this.value = value;
}
execute() {
this.receiver.subtract(this.value);
console.log(`Subtracted ${this.value}`);
}
undo() {
this.receiver.add(this.value);
console.log(`Undid subtract of ${this.value}`);
}
}
class Calculator {
constructor() {
this.value = 0;
}
add(val) {
this.value += val;
console.log(`Current value: ${this.value}`);
}
subtract(val) {
this.value -= val;
console.log(`Current value: ${this.value}`);
}
}
// Usage
const calc = new Calculator();
const addCmd = new AddCommand(calc, 10);
const subCmd = new SubtractCommand(calc, 5);
addCmd.execute(); // Added 10, Current value: 10
subCmd.execute(); // Subtracted 5, Current value: 5
subCmd.undo(); // Undid subtract of 5, Current value: 10
addCmd.undo(); // Undid add of 10, Current value: 0
Undo/Redo Manager
class CommandManager {
constructor() {
this.history = [];
this.current = -1;
}
execute(command) {
// Remove any commands after current position
this.history = this.history.slice(0, this.current + 1);
command.execute();
this.history.push(command);
this.current++;
}
undo() {
if (this.current >= 0) {
this.history[this.current].undo();
this.current--;
} else {
console.log('Nothing to undo');
}
}
redo() {
if (this.current < this.history.length - 1) {
this.current++;
this.history[this.current].execute();
} else {
console.log('Nothing to redo');
}
}
getHistory() {
return this.history.map((cmd, i) => ({
command: cmd.constructor.name,
current: i === this.current
}));
}
}
// Usage
const manager = new CommandManager();
const calc = new Calculator();
manager.execute(new AddCommand(calc, 5));
manager.execute(new AddCommand(calc, 3));
manager.execute(new SubtractCommand(calc, 2));
console.log(manager.getHistory());
// [{ command: 'AddCommand', current: false },
// { command: 'AddCommand', current: false },
// { command: 'SubtractCommand', current: true }]
manager.undo(); // Undo subtract
manager.undo(); // Undo second add
manager.redo(); // Redo second add
Real-World Examples
Text Editor Commands
class TextEditor {
constructor() {
this.content = '';
}
insert(text, position) {
this.content =
this.content.slice(0, position) +
text +
this.content.slice(position);
}
delete(position, length) {
this.content =
this.content.slice(0, position) +
this.content.slice(position + length);
}
getContent() {
return this.content;
}
}
class InsertCommand extends Command {
constructor(editor, text, position) {
super();
this.editor = editor;
this.text = text;
this.position = position;
}
execute() {
this.editor.insert(this.text, this.position);
}
undo() {
this.editor.delete(this.position, this.text.length);
}
}
class DeleteCommand extends Command {
constructor(editor, position, length) {
super();
this.editor = editor;
this.position = position;
this.length = length;
this.deletedText = '';
}
execute() {
this.deletedText = this.editor.getContent()
.slice(this.position, this.position + this.length);
this.editor.delete(this.position, this.length);
}
undo() {
this.editor.insert(this.deletedText, this.position);
}
}
// Usage
const editor = new TextEditor();
const manager = new CommandManager();
manager.execute(new InsertCommand(editor, 'Hello', 0));
manager.execute(new InsertCommand(editor, ' World', 5));
console.log(editor.getContent()); // "Hello World"
manager.undo();
console.log(editor.getContent()); // "Hello"
manager.redo();
console.log(editor.getContent()); // "Hello World"
Command Queue
class CommandQueue {
constructor() {
this.queue = [];
this.isProcessing = false;
}
add(command) {
this.queue.push(command);
if (!this.isProcessing) {
this.process();
}
}
async process() {
this.isProcessing = true;
while (this.queue.length > 0) {
const command = this.queue.shift();
await command.execute();
}
this.isProcessing = false;
}
}
class AsyncCommand extends Command {
constructor(task, delay = 1000) {
super();
this.task = task;
this.delay = delay;
}
async execute() {
console.log(`Starting: ${this.task}`);
await new Promise(resolve => setTimeout(resolve, this.delay));
console.log(`Completed: ${this.task}`);
}
undo() {
console.log(`Undoing: ${this.task}`);
}
}
// Usage
const queue = new CommandQueue();
queue.add(new AsyncCommand('Task 1', 1000));
queue.add(new AsyncCommand('Task 2', 500));
queue.add(new AsyncCommand('Task 3', 800));
// Tasks execute sequentially
When to Use Command Pattern
Good use cases:
- Undo/redo functionality
- Command queuing and scheduling
- Transaction systems
- Macro recording and playback
- Job scheduling
- Logging and auditing
Benefits
- Decouples sender/receiver: Invoker doesn't know about receiver
- Supports undo/redo: Easy to implement reversible operations
- Command queuing: Queue and execute commands later
- Logging/auditing: Track all operations
- Macro commands: Combine multiple commands
Best Practices
1. Always implement both execute and undo
class MyCommand extends Command {
execute() {
// Do something
}
undo() {
// Reverse it
}
}
2. Store state needed for undo
class DeleteCommand extends Command {
execute() {
this.deletedData = this.receiver.getData();
this.receiver.delete();
}
undo() {
this.receiver.restore(this.deletedData);
}
}
3. Use command manager for history
const manager = new CommandManager();
manager.execute(command); // Adds to history
manager.undo();
manager.redo();
Key Takeaways
- Command encapsulates requests as objects
- Perfect for undo/redo functionality
- Supports queuing and logging
- Decouples invoker from receiver
- Easy to add new commands
- Store state needed for undo operations
- Use command manager for history tracking
- Common in text editors, graphics apps, transaction systems