Command Pattern

Turn a request into a stand-alone object that contains all information about the request

Understanding the Command Pattern

The Command pattern is a behavioral design pattern that turns a request into a stand-alone object that contains all information about the request. This transformation allows you to parameterize methods with different requests, delay or queue a request's execution, and support undoable operations.

Problem

Imagine you're working on a text editor application. You need to implement various operations like copy, paste, undo, etc. You could create a complex class with numerous methods to handle all these operations, but this would violate the Single Responsibility Principle and make your code hard to maintain.

Additionally, consider a UI application where buttons trigger different actions depending on the context. How can you connect these buttons to various operations without creating dependencies on specific functionality?

Solution

The Command pattern suggests encapsulating the request as an object. The key objects in this pattern include:

Structure

Command Pattern Structure

Participants

When to Use

Use the Command Pattern when:

Benefits

Real-World Uses

Implementation Example

Here's a JavaScript implementation of the Command pattern for a simple text editor:

// Command interface
class Command {
    execute() {
        throw new Error("Method 'execute()' must be implemented");
    }
    
    undo() {
        throw new Error("Method 'undo()' must be implemented");
    }
}

// Receiver
class TextEditor {
    constructor() {
        this.text = "";
    }
    
    insertText(text, position) {
        const before = this.text.substring(0, position);
        const after = this.text.substring(position);
        this.text = before + text + after;
        console.log(`Inserted "${text}" at position ${position}`);
        console.log(`Current text: "${this.text}"`);
    }
    
    deleteText(startPosition, endPosition) {
        const deletedText = this.text.substring(startPosition, endPosition);
        const before = this.text.substring(0, startPosition);
        const after = this.text.substring(endPosition);
        this.text = before + after;
        console.log(`Deleted "${deletedText}" from position ${startPosition} to ${endPosition}`);
        console.log(`Current text: "${this.text}"`);
        return deletedText;
    }
    
    getText() {
        return this.text;
    }
}

// Concrete Commands
class InsertTextCommand extends Command {
    constructor(textEditor, text, position) {
        super();
        this.textEditor = textEditor;
        this.text = text;
        this.position = position;
    }
    
    execute() {
        this.textEditor.insertText(this.text, this.position);
    }
    
    undo() {
        this.textEditor.deleteText(this.position, this.position + this.text.length);
    }
}

class DeleteTextCommand extends Command {
    constructor(textEditor, startPosition, endPosition) {
        super();
        this.textEditor = textEditor;
        this.startPosition = startPosition;
        this.endPosition = endPosition;
        this.deletedText = null;
    }
    
    execute() {
        this.deletedText = this.textEditor.deleteText(this.startPosition, this.endPosition);
    }
    
    undo() {
        if (this.deletedText !== null) {
            this.textEditor.insertText(this.deletedText, this.startPosition);
        }
    }
}

// Invoker
class CommandHistory {
    constructor() {
        this.history = [];
        this.undoneCommands = [];
    }
    
    executeCommand(command) {
        command.execute();
        this.history.push(command);
        // Clear redo stack when a new command is executed
        this.undoneCommands = [];
    }
    
    undo() {
        if (this.history.length > 0) {
            const command = this.history.pop();
            command.undo();
            this.undoneCommands.push(command);
        } else {
            console.log("Nothing to undo");
        }
    }
    
    redo() {
        if (this.undoneCommands.length > 0) {
            const command = this.undoneCommands.pop();
            command.execute();
            this.history.push(command);
        } else {
            console.log("Nothing to redo");
        }
    }
}

// Usage
const textEditor = new TextEditor();
const history = new CommandHistory();

// Execute commands
history.executeCommand(new InsertTextCommand(textEditor, "Hello", 0));
history.executeCommand(new InsertTextCommand(textEditor, " World", 5));
history.executeCommand(new DeleteTextCommand(textEditor, 5, 11));
history.executeCommand(new InsertTextCommand(textEditor, " JavaScript", 5));

// Undo and redo
history.undo(); // Undo the last InsertTextCommand
history.undo(); // Undo the DeleteTextCommand
history.redo(); // Redo the DeleteTextCommand

In this example, we've implemented a simple text editor with insert and delete functionality. The Command pattern allows us to execute these operations and also provides undo/redo capability by tracking the history of executed commands.

Interactive Demo

Experience the Command pattern in action with this interactive text editor demo. You can type text, delete selections, and undo/redo your actions. Each operation is encapsulated as a Command object.

Interactive Demo: Text Editor with Undo/Redo

Type in the text area below and use the buttons to manipulate the text. Notice how each action creates a command object that can be undone or redone.

Command History:

// Command history will be displayed here