The Command design pattern is a behavioural design pattern that turns a request into a stand-alone object containing all information about the request. This transformation allows parameterizing methods with different requests, queuing requests, logging them, and supporting undoable operations. The key components of this pattern are the Command, the Receiver, the Invoker, and the Client.

Example Scenario 1:

Implementation:

Let's implement a simple example where we have a set of operations (commands) that can be executed on a text editor (receiver).

// Command interface
class Command {
  execute() {}
  undo() {}
}

// Concrete commands
class AddTextCommand extends Command {
  constructor(receiver, text) {
    super();
    this.receiver = receiver;
    this.text = text;
  }
  execute() {
    this.receiver.addText(this.text);
  }
  undo() {
    this.receiver.removeText(this.text);
  }
}

class RemoveTextCommand extends Command {
  constructor(receiver, text) {
    super();
    this.receiver = receiver;
    this.text = text;
  }
  execute() {
    this.receiver.removeText(this.text);
  }
  undo() {
    this.receiver.addText(this.text);
  }
}

// Receiver
class TextEditor {
  constructor() {
    this.content = "";
  }
  addText(text) {
    this.content += text;
    console.log(`Current content: "${this.content}"`);
  }
  removeText(text) {
    this.content = this.content.replace(new RegExp(`{text}`), "");
    console.log(`Current content: "${this.content}"`);
  }
}

// Invoker
class TextEditorInvoker {
  constructor() {
    this.history = [];
  }
  executeCommand(command) {
    command.execute();
    this.history.push(command);
  }
  undo() {
    if (this.history.length > 0) {
      const command = this.history.pop();
      command.undo();
    }
  }
}

Test case

// Client
const editor = new TextEditor();
const invoker = new TextEditorInvoker();

const addHelloCommand = new AddTextCommand(editor, "Hello ");
const addWorldCommand = new AddTextCommand(editor, "World!");

invoker.executeCommand(addHelloCommand);
invoker.executeCommand(addWorldCommand);

invoker.undo();
invoker.undo();

Explanation

In this implementation, we have a TextEditor class that acts as the receiver, knowing how to add and remove text. The Command interface defines the execute and undo methods. Concrete commands like AddTextCommand and RemoveTextCommand implement these methods and bind the receiver with a specific action.

The TextEditorInvoker class is responsible for executing commands and maintaining a history of executed commands for undo functionality. In the client code, we create commands to add "Hello" and "World!" to the text editor and then execute these commands using the invoker. The invoker adds the text to the editor and logs the current content. When we call undo, the invoker undoes the last command, removing the text in the reverse order it was added, thereby restoring the previous state of the text editor.

Example Scenario 2:

Let's create a more complex implementation of the Command design pattern by simulating a simple remote control system for home automation. This system will allow you to turn on and off multiple devices like lights and fans.

Implementation:

// Command interface
class Command {
  execute() {}
  undo() {}
}

// Concrete commands
class LightOnCommand extends Command {
  constructor(light) {
    super();
    this.light = light;
  }
  execute() {
    this.light.on();
  }
  undo() {
    this.light.off();
  }
}

class LightOffCommand extends Command {
  constructor(light) {
    super();
    this.light = light;
  }
  execute() {
    this.light.off();
  }
  undo() {
    this.light.on();
  }
}

class FanOnCommand extends Command {
  constructor(fan) {
    super();
    this.fan = fan;
  }
  execute() {
    this.fan.on();
  }
  undo() {
    this.fan.off();
  }
}

class FanOffCommand extends Command {
  constructor(fan) {
    super();
    this.fan = fan;
  }
  execute() {
    this.fan.off();
  }
  undo() {
    this.fan.on();
  }
}

// Receiver classes
class Light {
  on() {
    console.log("The light is on");
  }
  off() {
    console.log("The light is off");
  }
}

class Fan {
  on() {
    console.log("The fan is on");
  }
  off() {
    console.log("The fan is off");
  }
}

// Invoker
class RemoteControl {
  constructor() {
    this.history = [];
    this.undoHistory = [];
  }
  executeCommand(command) {
    command.execute();
    this.history.push(command);
  }
  undo() {
    if (this.history.length > 0) {
      const command = this.history.pop();
      command.undo();
      this.undoHistory.push(command);
    }
  }
  redo() {
    if (this.undoHistory.length > 0) {
      const command = this.undoHistory.pop();
      command.execute();
      this.history.push(command);
    }
  }
}

Test case