Command Pattern

Command Pattern involves encapsulating operations as executable commands and could either be in the form of objects or functions in JavaScript. It is common that we may want to make operations rely on certain context and states that are not accessible for the invokers. By storing those pieces of information with a command and passing it out, this situation could be properly handled.

Consider an extremely simple example: we want to provide a function called wait, which returns a cancel handler:

function wait() { 
  let $layer = $('.wait-layer'); 
   
  $layer.show(); 
   
  return () => { 
    $layer.hide(); 
  }; 
} 
 
let cancel = wait(); 
 
setTimeout(() => cancel(), 1000); 

The cancel handler in the preceding code is just a command we were talking about. It stores the context ($layer) using closure and is passed out as the return value of function wait.

Closure in JavaScript provides a really simple way to store command context and states, however, the direct disadvantage would be compromised flexibility between context/states and command functions because closure is lexically determined and cannot be changed at runtime. This would be okay if the command is only expected to be invoked with fixed context and states, but for more complex situations, we might need to construct them as objects with a proper data structure.

The following diagram shows the overall relations between participants of Command Pattern:

Command Pattern

By properly splitting apart context and states with the command object, Command Pattern could also play well with Flyweight Pattern if you wanted to reuse command objects multiple times.

Other common extensions based on Command Pattern include undo support and macros with multiple commands. We are going to play with those later in the implementation part.

Participants

The participants of Command Pattern include:

  • Command: Defines the general interface of commands passing around, it could be a function signature if the commands are in the form of functions.
  • Concrete command: Defines the specific behaviors and related data structure. It could also be a function that matches the signature declared as Command. The cancel handler in the very first example is a concrete command.
  • Context: The context or receiver that the command is associated with. In the first example, it is the $layer.
  • Client: Creates concrete commands and their contexts.
  • Invoker: Executes concrete commands.

Pattern scope

Command Pattern suggests two separate parts in a single application or a larger system: client and invoker. In the simplified example wait and cancel, it could be hard to distinguish the difference between those parts. But the line is clear: client knows or controls the context of commands to be executed with, while invoker does not have access or does not need to care about that information.

The key to the Command Pattern is the separation and bridging between those two parts through commands that store context and states.

Implementation

It's common for an editor to expose commands for third-party extensions to modify the text content. Consider a TextContext that contains information about the text file being edited and an abstract TextCommand class associated with that context:

class TextContext { 
  content = 'text content'; 
} 
 
abstract class TextCommand { 
  constructor( 
    public context: TextContext 
  ) { } 
 
  abstract execute(...args: any[]): void; 
} 

Certainly, TextContext could contain much more information like language, encoding, and so on. You can add them in your own implementation for more functionality. Now we are going to create two commands: ReplaceCommand and InsertCommand.

class ReplaceCommand extends TextCommand { 
  execute(index: number, length: number, text: string): void { 
    let content = this.context.content; 
 
    this.context.content = 
      content.substr(0, index) + 
      text + 
      content.substr(index + length); 
  } 
} 
 
class InsertCommand extends TextCommand { 
  execute(index: number, text: string): void { 
    let content = this.context.content; 
 
    this.context.content = 
      content.substr(0, index) + 
      text + 
      content.substr(index); 
  } 
} 

Those two commands share similar logic and actually InsertCommand can be treated as a subset of ReplaceCommand. Or if we have a new delete command, then replace command can be treated as the combination of delete and insert commands.

Now let's assemble those commands with the client and invoker:

class Client { 
  private context = new TextContext(); 
 
  replaceCommand = new ReplaceCommand(this.context); 
  insertCommand = new InsertCommand(this.context); 
} 
 
let client = new Client(); 
 
$('.replace-button').click(() => { 
  client.replaceCommand.execute(0, 4, 'the'); 
}); 
 
$('.insert-button').click(() => { 
  client.insertCommand.execute(0, 'awesome '); 
}); 

If we go further, we can actually have a command that executes other commands. Namely, we can have macro commands. Though the preceding example alone does not make it necessary to create a macro command, there would be scenarios where macro commands help. As those commands are already associated with their contexts, a macro command usually does not need to have an explicit context:

interface TextCommandInfo { 
  command: TextCommand, 
  args: any[]; 
} 
 
class MacroTextCommand { 
  constructor( 
    public infos: TextCommandInfo[] 
  ) { } 
 
  execute(): void { 
    for (let info of this.infos) { 
      info.command.execute(...info.args); 
    } 
  } 
} 

Consequences

Command Pattern decouples the client (who knows or controls context) and the invoker (who has no access to or does not care about detailed context).

It plays well with Composite Pattern. Consider the example of macro commands we mentioned above: a macro command can have other macro commands as its components, thus we make it a composite command.

Another important case of Command Pattern is adding support for undo operations. A direct approach is to add the undo method to every command. When an undo operation is requested, invoke the undo method of commands in reverse order, and we can pray that every command would be undone correctly. However, this approach relies heavily on a flawless implementation of the undo method as every mistake will accumulate. To implement more stable undo support, redundant information or snapshots could be stored.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.74.211