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:
The typical organization of the Command pattern can be described as follows:
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 great explanation of how OT works can be found at http://www.codecommit.com/blog/java/understanding-and-applying-operational-transformation.
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.
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.
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.
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:
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.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.
3.17.76.72