[Design Pattern] Behavioral Patterns - Command

ยท

5 min read

[Design Pattern] Behavioral Patterns - Command

๐Ÿ“Œ Behavioral Pattern

: Handles effective communication and responsibility assignment between objects.

๐Ÿ“Œ What is the Command Pattern?

It is a pattern that encapsulates the behavior (methods) of an object into a class.

  • When an object (A) wants to execute a method of another object (B), there is a dependency where object (A) needs to reference object (B). By applying the command pattern in such situations, we can remove the dependency. Additionally, when a functionality needs to be modified or changed, we can define a class for that functionality without modifying the code of class A, thus achieving system scalability and flexibility.

๐Ÿ“Œ Example Code

Let's take an example of a Google AI voice service called "OK Google". When you say "OK Google, turn on the heater," it turns on the heater.

  • Existing code
class Heater {
  powerOn() {
    console.log("Heater on");
  }
}

class OKGoogle {
  constructor(heater) {
    this.heater = heater;
  } // It needs to reference the Heater class.

  talk() {
    this.heater.powerOn();
  }
}

class Client {
  static main() {
    const heater = new Heater();
    const okGoogle = new OKGoogle(heater);
    okGoogle.talk();
  }
}

Client.main();
  • New action (method) - Adding a function to turn on the lamp
class Heater { // Existing Heater code
  powerOn() {
    console.log("Heater on");
  }
}

class Lamp { // New Lamp code
  turnOn() {
    console.log("Lamp on");
  }
}

class OKGoogle {
  static modes = ["heater", "lamp"];

  constructor(heater, lamp) {
    this.heater = heater; // It needs to reference the Heater class.
    this.lamp = lamp; // It also needs to reference the Lamp class.
    this.mode = "";
  }

  setMode(idx) {
    this.mode = OKGoogle.modes[idx];
  }

  talk() {
    switch (this.mode) {
      case "heater":
        this.heater.powerOn();
        break;
      case "lamp":
        this.lamp.turnOn();
        break;
    }
  }
}

class Client {
  static main() {
    const heater = new Heater();
    const lamp = new Lamp();
    const okGoogle = new OKGoogle(heater, lamp);

    // Turn on the lamp
    okGoogle.setMode(0);
    okGoogle.talk();

    // Set off the alarm
    okGoogle.setMode(1);
    okGoogle.talk();
  }
}

Client.main();

\=> Problem: OKGoogle needs to reference the Heater and Lamp objects to turn on the heater and lamp, and as the functionalities of OKGoogle increase, the object properties will increase, and the branching in the talk() method will also increase. This violates the Open-Closed Principle, where objects should be open to extension but closed to modification.

Applying the Command Design Pattern

  1. Create classes for various functionalities that OkGoogle can perform and encapsulate each functionality using the Command pattern (HeaterOnCommand, LampOnCommand).
// Command interface
class Command {
  run() {}
}

// HeaterOnCommand class
class HeaterOnCommand extends Command {
  constructor(heater) {
    super();
    this.heater = heater;
  }

  run() {
    this.heater.powerOn();
  }
}

// Heater class
class Heater {
  powerOn() {
    console.log("Heater on");
  }
}

// LampOnCommand class
class LampOnCommand extends Command {
  constructor(lamp) {
    super();
    this.lamp = lamp;
  }

  run() {
    this.lamp.turnOn();
  }
}

// Lamp class
class Lamp {
  turnOn() {
    console.log("Lamp on");
  }
}

// OKGoogle class
class OKGoogle {
  setCommand(command) {
    this.command = command;
  }

  talk() {
    this.command.run(); // no need to use if condition for talk()
  }
}

// Client class
class Client {
  static main() {
    const heater = new Heater();
    const lamp = new Lamp();

    const heaterOnCommand = new HeaterOnCommand(heater);
    const lampOnCommand = new LampOnCommand(lamp);
    const okGoogle = new OKGoogle();

    // turn on heater
    okGoogle.setCommand(heaterOnCommand);
    okGoogle.talk();

    // turn on lamp
    okGoogle.setCommand(lampOnCommand);
    okGoogle.talk();
  }
}

Client.main();

\=> The Command pattern allows you to encapsulate requests (e.g., Heater, Lamp) as objects, separating them from the execution (OkGoogle). This reduces the coupling between the object making the request and the one performing the action, improving code flexibility and scalability.

๐Ÿ“Œ When to Use

The Command pattern is useful in the following scenarios:

  1. When you need to separate requests from their execution: The Command pattern encapsulates requests as objects, decoupling the sender (OkGoogle) from the receiver objects (Heater, Lamp). This enhances code flexibility and extensibility by reducing the direct coupling between the sender and specific functionalities.

  2. When you need undo or redo functionality: Since the Command objects encapsulate performed actions, it becomes easy to implement undo and redo functionality. You can store previously executed Command objects and control the actions by executing or undoing the respective commands as needed.

  3. When you need to manage commands in a queue or log: The Command pattern allows you to store Command objects in a queue or log, enabling you to manage the order of operations and retrieve or re-execute commands when necessary.

  4. When you need to ensure the consistency and safety of operations: By encapsulating operations as Command objects, the Command pattern ensures consistency and safety. Exception handling or rollback mechanisms can be implemented within the Command objects to handle exceptions or failures during the execution of operations, providing transaction-like behavior.

๐Ÿ“Œ How to Use

To effectively use the Command pattern:

  • Declare the Command interface with a single execution method (run()).

  • Implement concrete command classes that adhere to the Command interface, encapsulating the receiver object and any necessary arguments.

  • Identify the classes that will act as senders (clients). Add fields in these classes to store the commands and communicate with the commands through the Command interface.

  • Instead of directly sending requests to the receiver objects, invoke the commands to execute the requested actions.

  • Initialize the objects in the following order:

    1. Create receiver objects.

    2. Create command objects and associate them with the respective receiver objects if needed.

    3. Create sender objects and associate them with specific command objects.

By following these guidelines, you can effectively use the Command pattern to encapsulate requests, decouple senders from receivers, and enhance the flexibility and maintainability of your code.

ย