Skip to main content

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
🔍 Three Implementation Approaches:
  • 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.com

Concepts

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"