top of page
Writer's pictureThe Tech Platform

Command Pattern in Typescript

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


In object-oriented programming, the command pattern is a behavioral design pattern in which an object is used to represent and encapsulate all the information needed to call a method at a later time. This information includes the method name, the object that owns the method and values for the method parameters.


Four terms always associated with the command pattern are command, receiver, invoker and client. A command object has a receiver object and invokes a method of the receiver in a way that is specific to that receiver’s class. The receiver then does the work. A command object is separately passed to an invoker object, which invokes the command, and optionally does bookkeeping about the command execution. Any command object can be passed to the same invoker object. Both an invoker object and several command objects are held by a client object. The client contains the decision making about which commands to execute at which points. To execute a command, it passes the command object to the invoker object.


Using command objects makes it easier to construct general components that need to delegate, sequence or execute method calls at a time of their choosing without the need to know the class of the method or the method parameters. Using an invoker object allows bookkeeping about command executions to be conveniently performed, as well as implementing different modes for commands, which are managed by the invoker object, without the need for the client to be aware of the existence of bookkeeping or modes.


Design principles utilized by the command pattern

Separation of concerns: The remote will know how to make a request but is not concerned with what happens or how the request is executed.

Programming to an Interface : Because each request is an implementation of the same interface, it allows new command objects to be created independently without altering any previously written code.

Low coupling: The command objects only interact with other classes when a request is made for a particular action without sharing any other information. Due to this, changes in one class’s implementation won’t affect how the other class communicates with it.


Class diagram depicting a generic command pattern implementation.




Use cases for the Command Pattern

  • Plugin development. Consider what a plugin is: an encapsulated behavior that you can inject into existing client code that will execute it, without really having any context about its internal logic. That fits perfectly inside the command pattern.

  • Undo actions. Every command has a very distinct logic and it can also keep a state associated with that logic. For example, the state of the object you’re using or modifying through your command right before the command’s execute method is called. That way, you can also add an undo method, that will reverse the changes done by the command. This pattern makes implementing the “Undo” functionality surprisingly easy.

  • Creating workflows. Imagine having to perform multiple tasks in a set order. Or wanting the ability to queue multiple actions and execute them one by one. That can easily be done by encapsulating said actions into their own objects and then dealing with them as if they were “first-class citizens” of your problem domain. That is the command pattern right there. The client code is used to orchestrate a set of independent commands thanks to the fact that they all share the same interface.

  • UI actions that are independent of their visual component. Put another way, you can save a file by pressing CTRL+S or going to the File menu, then clicking on “Save”, or you can even click on the floppy disk icon on your menu bar. The action remains the same: saving the file locally, but the starting point is different. That can be achieved by encapsulating the underlying logic of the action in one place (a Command) and having its execution be triggered from wherever it might be needed. The “hard part” here is coding the action’s logic, the rest is just calling a method.'



Example

Below is an example code of the command pattern. The Television class is a simple object and the Remote class acts as the caller, using the TelevisionOnCommand and TelevisionOffCommand classes to interact with the Television objects.


The TelevisionOnCommand and TelevisionOffCommand classes are concrete implementations of the Command interface.

// the television class
export class Television 
{ 
  state: boolean = false;

  on() {
    this.state = true;
  }

  off() {
    this.state = false;
  }
}

// the command interface
interface Command { 
  execute(): any;
  undo(): any;
}

// the televisiononcommand is a concrete implementation of the command interface
class TelevisionOnCommand implements Command {
  television: Television;

  constructor(television: Television) {
    this.television = television;
  }

  execute() {
    this.television.on();
  }

  undo() {
    this.television.off();
  }
}

// the televisionoffcommand is a concrete implementation of the command interface
class TelevisionOffCommand implements Command {
  television: Television;

  constructor(television: Television) {
    this.television = television;
  }

  execute() {
    this.television.off();
  }

  undo() {
    this.television.on();
  }
}

// the remote in this case is the caller
class Remote {
  onCommand: Command;
  offCommand: Command;

  setCommand(onCommand, offCommand) {
    this.onCommand = onCommand;
    this.offCommand = offCommand;
  }

  onButtonClick() {
    this.onCommand.execute();
  }

  offButtonClick() {
    this.offCommand.execute()
  }
}


let television = new Television();
let televisionOnCommand = new TelevisionOnCommand(television);
let televisionOffCommand = new TelevisionOffCommand(television);
let remote = new Remote();

remote.setCommand(televisionOnCommand, televisionOffCommand);

console.log('state of television before remote is used:', television.state);
remote.onButtonClick();
console.log('state of television after remote is used:', television.state);



When to Use Command Pattern

  • Use the Command pattern when you want to parametrize objects with operations.

  • Use the Command pattern when you want to queue operations, schedule their execution, or execute them remotely.

  • Use the Command pattern when you want to implement reversible operations.



The Tech Platform

0 comments

Comentarios


bottom of page