Design Patterns: Command Pattern

ClintClint
4 min read

The Command pattern is a behavioral design pattern that encapsulates a request as an object, thereby allowing for the separation of the requester from the object that performs the action. This pattern is suitable for projects where there is a need to decouple the sender and receiver of a request, and where there is a need for undo or redo functionality.

One example of a project where the Command pattern is useful is in the development of an image editor that allows users to perform different operations on images, such as cropping, resizing, or applying filters. By using the Command pattern, each operation can be encapsulated as a separate object, which can be stored in a command history. This makes it possible to undo or redo operations, as well as to apply multiple operations in sequence. This can help to improve the usability and flexibility of the image editor and make it easier to add or modify operations in the future.

Another example of a project where the Command pattern is useful is in the development of a game engine that allows players to perform different actions, such as moving, attacking, or casting spells. By using the Command pattern, each action can be encapsulated as a separate object, which can be stored in a command queue. This makes it possible to execute actions in a specific order and to undo or redo actions as needed. This can help to improve the gameplay and balance of the game and make it easier to add or modify actions in the future.

In general, the Command pattern should be used in projects where there is a need to decouple the sender and receiver of a request, and where there is a need for undo or redo functionality. It can help to improve the flexibility and maintainability of the code, by reducing the coupling between the components and making it easier to add or modify commands over time.

Let's take a look at the Command Design Pattern in action.

// Command Interface
class Command {
  execute() {
    throw new Error('execute method must be implemented');
  }

  undo() {
    throw new Error('undo method must be implemented');
  }

  redo() {
    throw new Error('redo method must be implemented');
  }
}

// Concrete Commands
class AddTextCommand extends Command {
  constructor(editor, text, position) {
    super();
    this.editor = editor;
    this.text = text;
    this.position = position;
    this.removedText = '';
  }

  execute() {
    this.removedText = this.editor.addText(this.text, this.position);
  }

  undo() {
    this.editor.removeText(this.position, this.text.length);
    this.editor.addText(this.removedText, this.position);
  }

  redo() {
    this.execute();
  }
}

class RemoveTextCommand extends Command {
  constructor(editor, position, length) {
    super();
    this.editor = editor;
    this.position = position;
    this.length = length;
    this.removedText = '';
  }

  execute() {
    this.removedText = this.editor.removeText(this.position, this.length);
  }

  undo() {
    this.editor.addText(this.removedText, this.position);
  }

  redo() {
    this.execute();
  }
}

// Receiver
class TextEditor {
  constructor() {
    this.text = '';
    this.commandHistory = [];
    this.undoIndex = -1;
  }

  addText(text, position) {
    this.text = this.text.slice(0, position) + text + this.text.slice(position);
    return this.text.slice(position, position + text.length);
  }

  removeText(position, length) {
    const removedText = this.text.slice(position, position + length);
    this.text = this.text.slice(0, position) + this.text.slice(position + length);
    return removedText;
  }

  executeCommand(command) {
    command.execute();
    this.commandHistory.push(command);
    this.undoIndex = this.commandHistory.length - 1;
  }

  undo() {
    if (this.undoIndex >= 0) {
      const command = this.commandHistory[this.undoIndex];
      command.undo();
      this.undoIndex--;
    }
  }

  redo() {
    if (this.undoIndex < this.commandHistory.length - 1) {
      this.undoIndex++;
      const command = this.commandHistory[this.undoIndex];
      command.redo();
    }
  }
}

// Usage
const textEditor = new TextEditor();

const addCommand = new AddTextCommand(textEditor, 'Hello, world!', 0);
textEditor.executeCommand(addCommand);

const removeCommand = new RemoveTextCommand(textEditor, 0, 6);
textEditor.executeCommand(removeCommand);

console.log(textEditor.text); // Output: world!

textEditor.undo();
console.log(textEditor.text); // Output: Hello, world!

textEditor.redo();
console.log(textEditor.text); // Output: world!

In the above code, we define a Command interface that specifies the execute, undo, and redo methods that must be implemented by concrete command classes. We then implement two concrete commands, `AddTextCommand and RemoveTextCommand, that adds or removes text from the text editor, respectively. Each command stores the relevant information needed to execute, undo, and redo the action.

We also define a TextEditor class that serves as the receiver of the commands. The TextEditor class has a commandHistory array to store all the executed commands and a undoIndex variable to keep track of the index of the last executed command. It also has executeCommand, undo, and redo methods that handle executing, undoing, and redoing commands respectively.

In the usage example, we create an instance of TextEditor, executes two commands to add and remove text, and then undo and redo the commands to restore the original text. This example demonstrates how the Command pattern can be used to implement undo and redo functionality in a text editor app.

0
Subscribe to my newsletter

Read articles from Clint directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Clint
Clint

Writer, software engineer, content creator, and all-around awesome guy.