Book Image

Node.js Design Patterns

By : Mario Casciaro
Book Image

Node.js Design Patterns

By: Mario Casciaro

Overview of this book

Node.js is a massively popular software platform that lets you use JavaScript to easily create scalable server-side applications. It allows you to create efficient code, enabling a more sustainable way of writing software made of only one language across the full stack, along with extreme levels of reusability, pragmatism, simplicity, and collaboration. Node.js is revolutionizing the web and the way people and companies create their software. In this book, we will take you on a journey across various ideas and components, and the challenges you would commonly encounter while designing and developing software using the Node.js platform. You will also discover the "Node.js way" of dealing with design and coding decisions. The book kicks off by exploring the fundamental principles and components that define the platform. It then shows you how to master asynchronous programming and how to design elegant and reusable components using well-known patterns and techniques. The book rounds off by teaching you the various approaches to scale, distribute, and integrate your Node.js application.
Table of Contents (16 chapters)
Node.js Design Patterns
Credits
About the Author
Acknowledgments
About the Reviewers
www.PacktPub.com
Preface
Index

The observer pattern


Another important and fundamental pattern used in Node.js is the observer pattern. Together with reactor, callbacks, and modules, this is one of the pillars of the platform and an absolute prerequisite for using many node-core and userland modules.

Observer is an ideal solution for modeling the reactive nature of Node.js, and a perfect complement for callbacks. Let's give a formal definition as follows:

Note

Pattern (observer): defines an object (called subject), which can notify a set of observers (or listeners), when a change in its state happens.

The main difference from the callback pattern is that the subject can actually notify multiple observers, while a traditional continuation-passing style callback will usually propagate its result to only one listener, the callback.

The EventEmitter

In traditional object-oriented programming, the observer pattern requires interfaces, concrete classes, and a hierarchy; in Node.js, all becomes much simpler. The observer pattern is already built into the core and is available through the EventEmitter class. The EventEmitter class allows us to register one or more functions as listeners, which will be invoked when a particular event type is fired. The following image visually explains the concept:

The EventEmitter is a prototype, and it is exported from the events core module. The following code shows how we can obtain a reference to it:

var EventEmitter = require('events').EventEmitter;
var eeInstance = new EventEmitter();

The essential methods of the EventEmitter are given as follows:

  • on(event, listener): This method allows you to register a new listener (a function) for the given event type (a string)

  • once(event, listener): This method registers a new listener, which is then removed after the event is emitted for the first time

  • emit(event, [arg1], […]): This method produces a new event and provides additional arguments to be passed to the listeners

  • removeListener(event, listener): This method removes a listener for the specified event type

All the preceding methods will return the EventEmitter instance to allow chaining. The listener function has the signature, function([arg1], […]), so it simply accepts the arguments provided the moment the event is emitted. Inside the listener, this refers to the instance of the EventEmitter that produces the event.

We can already see that there is a big difference between a listener and a traditional Node.js callback; in particular, the first argument is not an error, but it can be any data passed to emit() at the moment of its invocation.

Create and use an EventEmitter

Let's see how we can use an EventEmitter in practice. The simplest way is to create a new instance and use it directly. The following code shows a function, which uses an EventEmitter to notify its subscribers in real time when a particular pattern is found in a list of files:

var EventEmitter = require('events').EventEmitter;
var fs = require('fs');

function findPattern(files, regex) {
  var emitter = new EventEmitter();
  files.forEach(function(file) {
    fs.readFile(file, 'utf8', function(err, content) {
      if(err)
        return emitter.emit('error', err);
      
      emitter.emit('fileread', file);
      var match = null;
      if(match = content.match(regex))
        match.forEach(function(elem) {
          emitter.emit('found', file, elem);
        });
    });
  });
  return emitter;
}

The EventEmitter created by the preceding function will produce the following three events:

  • fileread: This event occurs when a file is read

  • found: This event occurs when a match has been found

  • error: This event occurs when an error has occurred during the reading of the file

Let's see now how our findPattern() function can be used:

findPattern(
    ['fileA.txt', 'fileB.json'],
    /hello \w+/g
  )
  .on('fileread', function(file) {
    console.log(file + ' was read');
  })
  .on('found', function(file, match) {
    console.log('Matched "' + match + '" in file ' + file);
  })
  .on('error', function(err) {
    console.log('Error emitted: ' + err.message);
  });

In the preceding example, we registered a listener for each of the three event types produced by the EventEmitter which was created by our findPattern() function.

Propagating errors

The EventEmitter - as it happens for callbacks - cannot just throw exceptions when an error condition occurs, as they would be lost in the event loop if the event is emitted asynchronously. Instead, the convention is to emit a special event, called error, and to pass an Error object as an argument. That's exactly what we are doing in the findPattern() function that we defined earlier.

Note

It is always a good practice to register a listener for the error event, as Node.js will treat it in a special way and will automatically throw an exception and exit from the program if no associated listener is found.

Make any object observable

Sometimes, creating a new observable object directly from the EventEmitter class is not enough, as this makes it impractical to provide functionality that goes beyond the mere production of new events. It is more common, in fact, to have the need to make a generic object observable; this is possible by extending the EventEmitter class.

To demonstrate this pattern, let's try to implement the functionality of the findPattern() function in an object as follows:

var EventEmitter = require('events').EventEmitter;
var util = require('util');
var fs = require('fs');

function FindPattern(regex) {
  EventEmitter.call(this);
  this.regex = regex;
  this.files = [];
}
util.inherits(FindPattern, EventEmitter);

FindPattern.prototype.addFile = function(file) {
  this.files.push(file);
  return this;
};

FindPattern.prototype.find = function() {
  var self = this;
  self.files.forEach(function(file) {
    fs.readFile(file, 'utf8', function(err, content) {
      if(err)
        return self.emit('error', err);
      
      self.emit('fileread', file);
      var match = null;
      if(match = content.match(self.regex))
        match.forEach(function(elem) {
          self.emit('found', file, elem);
        });
    });
  });
  return this;
};

The FindPattern prototype that we defined extends the EventEmitter using the inherits() function provided by the core module util. This way, it becomes a full-fledged observable class. The following is an example of its usage:

var findPatternObject = new FindPattern(/hello \w+/);
findPatternObject
  .addFile('fileA.txt')
  .addFile('fileB.json')
  .find()
  .on('found', function(file, match) {
    console.log('Matched "' + match + '" in file ' + file);
  })
  .on('error', function(err) {
    console.log('Error emitted ' + err.message);
  });

We can now see how the FindPattern object has a full set of methods, in addition to being observable by inheriting the functionality of the EventEmitter.

This is a pretty common pattern in the Node.js ecosystem, for example, the Server object of the core http module defines methods such as listen(), close(), setTimeout(), and internally it also inherits from the EventEmitter function, thus allowing it to produce events, such as request, when a new request is received, or connection, when a new connection is established, or closed, when the server is closed.

Other notable examples of objects extending the EventEmitter are Node.js streams. We will analyze streams in more detail in Chapter 3, Coding with Streams.

Synchronous and asynchronous events

As with callbacks, events can be emitted synchronously or asynchronously, and it is crucial that we never mix the two approaches in the same EventEmitter, but even more importantly, when emitting the same event type, to avoid to produce the same problems that we described in the Unleashing Zalgo section.

The main difference between emitting synchronous or asynchronous events lies in the way listeners can be registered. When the events are emitted asynchronously, the user has all the time to register new listeners even after the EventEmitter is initialized, because the events are guaranteed not to be fired until the next cycle of the event loop. That's exactly what is happening in the findPattern() function. We defined this function previously and it represents a common approach that is used in most Node.js modules.

On the contrary, emitting events synchronously requires that all the listeners are registered before the EventEmitter function starts to emit any event. Let's look at an example:

function SyncEmit() {
  this.emit('ready');
}
util.inherits(SyncEmit, EventEmitter);

var syncEmit = new SyncEmit();
syncEmit.on('ready', function() {
  console.log('Object is ready to be used');
});

If the ready event was emitted asynchronously, then the previous code would work perfectly; however, the event is produced synchronously and the listener is registered after the event was already sent, so the result is that the listener is never invoked; the code will print nothing to the console.

Contrarily to callbacks, there are situations where using an EventEmitter in a synchronous fashion makes sense, given its different purpose. For this reason, it's very important to clearly highlight the behavior of our EventEmitter in its documentation to avoid confusion, and potentially a wrong usage.

EventEmitter vs Callbacks

A common dilemma when defining an asynchronous API is to check whether to use an EventEmitter or simply accept a callback. The general differentiating rule is semantic: callbacks should be used when a result must be returned in an asynchronous way; events should instead be used when there is a need to communicate that something has just happened.

But besides this simple principle, a lot of confusion is generated from the fact that the two paradigms are most of the time equivalent and allow you to achieve the same results. Consider the following code for an example:

function helloEvents() {
  var eventEmitter = new EventEmitter();
  setTimeout(function() {
    eventEmitter.emit('hello', 'world');
  }, 100);
  return eventEmitter;
}
function helloCallback(callback) {
  setTimeout(function() {
    callback('hello', 'world');
  }, 100);
}

The two functions helloEvents() and helloCallback() can be considered equivalent in terms of functionality; the first communicates the completion of the timeout using an event, the second uses a callback to notify the caller instead, passing the event type as an argument. But what really differentiates them is the readability, the semantic, and the amount of code that is required to be implemented or used. While we cannot give a deterministic set of rules to choose between one or the other style, we can certainly provide some hints to help take the decision.

As a first observation, we can say that callbacks have some limitations when it comes to supporting different types of events. In fact, we can still differentiate between multiple events by passing the type as an argument of the callback, or by accepting several callbacks, one for each supported event. However, this cannot exactly be considered an elegant API. In this situation, an EventEmitter can give a better interface and leaner code.

Another case where the EventEmitter might be preferable is when the same event can occur multiple times, or not occur at all. A callback, in fact, is expected to be invoked exactly once, whether the operation is successful or not. The fact that we have a possibly repeating circumstance should let us think again about the semantic nature of the occurrence, which is more similar to an event that has to be communicated rather than a result; in this case an EventEmitter is the preferred choice.

Lastly, an API using callbacks can notify only that particular callback, while using an EventEmitter function it's possible for multiple listeners to receive the same notification.

Combine callbacks and EventEmitter

There are also some circumstances where an EventEmitter can be used in conjunction with a callback. This pattern is extremely useful when we want to implement the principle of small surface area by exporting a traditional asynchronous function as the main functionality, while still providing richer features, and more control by returning an EventEmitter. One example of this pattern is offered by the node-glob module (https://npmjs.org/package/glob), a library that performs glob-style file searches. The main entry point of the module is the function it exports, which has the following signature:

glob(pattern, [options], callback)

The function takes pattern as the first argument, a set of options, and a callback function which is invoked with the list of all the files matching the provided pattern. At the same time, the function returns an EventEmitter that provides a more fine-grained report over the state of the process. For example, it is possible to be notified in real-time when a match occurs by listening to the match event, to obtain the list of all the matched files with the end event, or to know whether the process was manually aborted by listening to the abort event. The following code shows how this looks:

var glob = require('glob');
glob('data/*.txt', function(error, files) {
  console.log('All files found: ' + JSON.stringify(files));
}).on('match', function(match) {
  console.log('Match found: ' + match);
});

As we can see, the practice of exposing a simple, clean, and minimal entry point while still providing more advanced or less important features with secondary means is quite common in Node.js, and combining EventEmitter with traditional callbacks is one of the ways to achieve that.

Tip

Pattern: create a function that accepts a callback and returns an EventEmitter, thus providing a simple and clear entry point for the main functionality, while emitting more fine-grained events using the EventEmitter.