Command

Another design pattern with huge importance in Node.js is Command. In its most generic definition, we can consider a Command as any object that encapsulates all the information necessary to perform an action at a later time. So, instead of invoking a method or a function directly, we create an object representing the intention to perform such an invocation; it will then be the responsibility of another component to materialize the intent, transforming it into an actual action. Traditionally, this pattern is built around four major components, as shown in the following figure:

Command

The typical organization of the Command pattern can be described as follows:

  • Command: This is the object encapsulating the information necessary to invoke a method or function.
  • Client: This creates the command and provides it to the Invoker.
  • Invoker: This is responsible for executing the command on the target.
  • Target (or Receiver): This is the subject of the invocation. It can be a lone function or the method of an object.

As we will see, these four components can vary a lot depending on the way we want to implement the pattern; this should not sound new at this point.

Using the Command pattern instead of directly executing an operation has several advantages and applications:

  • A command can be scheduled for execution at a later time.
  • A command can be easily serialized and sent over the network. This simple property allows us to distribute jobs across remote machines, transmit commands from the browser to the server, create RPC systems, and so on.
  • Commands make it easy to keep a history of all the operations executed on a system.
  • Commands are an important part of some algorithms for data synchronization and conflict resolution.
  • A command scheduled for execution can be cancelled if it's not yet executed. It can also be reverted (undone), bringing the state of the application to the point before the command was executed.
  • Several commands can be grouped together. This can be used to create atomic transactions or to implement a mechanism whereby all the operations in the group are executed at once.
  • Different kinds of transformation can be performed on a set of commands, such as duplicate removal, joining and splitting, or applying more complex algorithms such as Operational Transformation (OT), which is the base for most of today's real-time collaborative software, such as collaborative text editing.

The preceding list clearly shows us how important this pattern is, especially in a platform such as Node.js where networking and asynchronous execution are essential players.

A flexible pattern

As we already mentioned, the Command pattern in JavaScript can be implemented in many different ways; we are now going to demonstrate only a few of them just to give an idea of its scope.

The task pattern

We can start off with the most basic and trivial implementation: the task pattern. The easiest way in JavaScript to create an object representing an invocation is, of course, by creating a closure:

function createTask(target, args) { 
  return () => { 
    target.apply(null, args); 
  } 
} 

This should not look new at all; we have used this pattern already so many times throughout the book, and in particular in Chapter 3, Asynchronous Control Flow Patterns with Callbacks. This technique allowed us to use a separate component to control and schedule the execution of our tasks, which is essentially equivalent to the Invoker of the Command pattern. For example, do you remember how we were defining tasks to pass to the async library? Or even better, do you remember how we were using thunks in combination with generators? The callback pattern itself can be considered a very simple version of the Command pattern.

A more complex command

Let's now work on an example of a more complex command; this time we want to support undo and serialization. Let's start with the target of our commands, a little object that is responsible for sending status updates to a service like Twitter. We use a mock-up of such a service for simplicity:

const statusUpdateService = { 
  statusUpdates: {}, 
  sendUpdate: function(status) { 
    console.log('Status sent: ' + status); 
    let id = Math.floor(Math.random() * 1000000); 
    statusUpdateService.statusUpdates[id] = status; 
    return id; 
  }, 
 
  destroyUpdate: id => { 
    console.log('Status removed: ' + id); 
    delete statusUpdateService.statusUpdates[id]; 
  } 
}; 

Now, let's create a command to represent the posting of a new status update:

function createSendStatusCmd(service, status) { 
  let postId = null; 
 
  const command = () => { 
    postId = service.sendUpdate(status); 
  }; 
 
  command.undo = () => { 
    if(postId) { 
      service.destroyUpdate(postId); 
      postId = null; 
    } 
  }; 
 
  command.serialize = () => { 
    return {type: 'status', action: 'post', status: status}; 
  }; 
 
  return command; 
} 

The preceding function is a factory that produces new sendStatus commands. Each command implements the following three functionalities:

  1. The command itself is a function that, when invoked, will trigger the action; in other words, it implements the task pattern that we have seen before. The command when executed will send a new status update using the methods of the target service.
  2. An undo() function, attached to the main task, that reverts the effects of the operations. In our case, we are simply invoking the destroyUpdate() method on the target service.
  3. A serialize() function that builds a JSON object that contains all the necessary information to reconstruct the same command object.

After this, we can build an Invoker; we can start by implementing its constructor and its run() method:

class Invoker { 
 
  constructor() { 
    this.history = []; 
  } 
 
  run (cmd) { 
    this.history.push(cmd); 
    cmd(); 
    console.log('Command executed', cmd.serialize()); 
  } 
} 

The run() method that we defined earlier is the basic functionality of our Invoker; it is responsible for saving the command into the history instance variable and then triggering the execution of the command itself. Next, we can add a new method that delays the execution of a command:

delay (cmd, delay) { 
  setTimeout(() => { 
    this.run(cmd); 
  }, delay) 
} 

Then, we can implement an undo() method that reverts the last command:

undo () { 
  const cmd = this.history.pop(); 
  cmd.undo(); 
  console.log('Command undone', cmd.serialize()); 
} 

Finally, we also want to be able to run a command on a remote server, by serializing and then transferring it over the network using a web service:

  runRemotely (cmd) { 
    request.post('http://localhost:3000/cmd', 
      {json: cmd.serialize()}, 
      err => { 
        console.log('Command executed remotely', cmd.serialize()); 
      } 
    ); 
  } 
} 

Now that we have the Command, the Invoker, and the Target, the only component missing is the Client. Let's start with instantiating Invoker:

const invoker = new Invoker(); 

Then, we can create a command using the following line of code:

const command = createSendStatusCmd(statusUpdateService, 'HI!'); 

We now have a command representing the posting of a status message; we can then decide to dispatch it immediately:

invoker.run(command); 

Oops, we made a mistake; let's revert to the state of our timeline as it was before sending the last message:

invoker.undo(); 

We can also decide to schedule the message to be sent in an hour from now:

invoker.delay(command, 1000 * 60 * 60); 

Alternatively, we can distribute the load of the application by migrating the task to another machine:

invoker.runRemotely(command); 

The little example that we have just created shows how wrapping an operation in a command can open a world of possibilities, and that's just the tip of the iceberg.

As the last remarks, it is worth noticing that a fully-fledged Command pattern has been used only when really needed. We saw, in fact, how much additional code we had to write to simply invoke a method of statusUpdateService; if all that we need is only an invocation, then a complex command would be overkill. If, however, we need to schedule the execution of a task, or run an asynchronous operation, then the simpler task pattern offers the best compromise. If instead, we need more advanced features such as undo support, transformations, conflict resolution, or one of the other fancy use cases that we described previously, using a more complex representation for the command is almost necessary.

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

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