Chapter 6. Design Patterns

A design pattern is a reusable solution to a recurring problem; the term is really broad in its definition and can span multiple domains of an application. However, the term is often associated with a well-known set of object-oriented patterns that were popularized in the 90s by the book, Design Patterns: Elements of Reusable Object-Oriented Software, Pearson Education, by the almost legendary Gang of Four (GoF): Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. We will often refer to these specific sets of patterns as traditional design patterns, or GoF design patterns.

Applying this set of object-oriented design patterns in JavaScript is not as linear and formal as it would be in a classical object-oriented language. As we know, JavaScript is multi-paradigm, object-oriented, and prototype-based, and has dynamic typing; it treats functions as first-class citizens, and allows functional programming styles. These characteristics make JavaScript a very versatile language, which gives tremendous power to the developer but at the same time it causes a fragmentation of programming styles, conventions, techniques, and ultimately the patterns of its ecosystem. There are so many ways to achieve the same result using JavaScript that everybody has their own opinion on what the best way is to approach a problem. A clear demonstration of this phenomenon is the abundance of frameworks and opinionated libraries in the JavaScript ecosystem; probably no other language has ever seen so many, especially now that Node.js has given new astonishing possibilities to JavaScript and has created so many new scenarios.

In this context, the traditional design patterns are affected by the nature of JavaScript too. There are so many ways in which they can be implemented that their traditional, strongly object-oriented implementation means they cease to be patterns. In some cases, they are not even possible, because JavaScript, as we know, doesn't have real classes or abstract interfaces. What doesn't change though is the original idea at the base of each pattern, the problem it solves, and the concepts at the heart of the solution.

In this chapter, we will see how some of the most important GoF design patterns apply to Node.js and its philosophy, thus rediscovering their importance from another perspective. Among these traditional patterns, we will also have a look at some "less traditional" design patterns generated within the JavaScript ecosystem.

The design patterns explored in this chapter are as follows:

  • Factory
  • Revealing constructor
  • Proxy
  • Decorator
  • Adapter
  • Strategy
  • State
  • Template
  • Middleware
  • Command

Note

This chapter assumes that the reader has some notion of how inheritance works in JavaScript. Please also be advised that throughout this chapter we will often use generic and more intuitive diagrams to describe a pattern in place of standard UML, since many patterns can have an implementation based not only on classes, but also on objects and even functions.

Factory

We begin our journey starting from what is probably the most simple and common design pattern in Node.js: factory.

A generic interface for creating objects

We already stressed the fact that, in JavaScript, the functional paradigm is often preferred to a purely object-oriented design, for its simplicity, usability, and small surface area. This is especially true when creating new object instances. In fact, invoking a factory, instead of directly creating a new object from a prototype using the new operator or Object.create(), is so much more convenient and flexible in several respects.

First and foremost, a factory allows us to separate the object creation from its implementation; essentially, a factory wraps the creation of a new instance giving us more flexibility and control in the way we do it. Inside the factory, we can create a new instance leveraging closures, using a prototype and the new operator, using Object.create(), or even returning a different instance based on a particular condition. The consumer of the factory is totally agnostic about how the creation of the instance is carried out. The truth is that, by using new, we are binding our code to one specific way of creating an object, while in JavaScript we can have much more flexibility, almost for free. As a quick example, let's consider a simple factory that creates an Image object:

function createImage(name) { 
  return new Image(name); 
} 
const image = createImage('photo.jpeg'); 

The createImage() factory might look totally unnecessary; why not instantiate the Image class by using the new operator directly? Something like the following line of code:

const image = new Image(name); 

As we already mentioned, using new binds our code to one particular type of object; in the preceding case, to objects of type Image. A factory instead gives us much more flexibility; imagine that we want to refactor the Image class, splitting it into smaller classes, one for each image format that we support. If we exposed a factory as the only means to create new images, we can simply rewrite it as follows, without breaking any of the existing code:

function createImage(name) { 
  if(name.match(/.jpeg$/)) { 
    return new JpegImage(name); 
  } else if(name.match(/.gif$/)) { 
    return new GifImage(name); 
  } else if(name.match(/.png$/)) { 
    return new PngImage(name); 
  } else { 
    throw new Exception('Unsupported format'); 
  } 
} 

Our factory also allows us to not expose the constructors of the objects it creates, and prevents them from being extended or modified (remember the principle of small surface area?). In Node.js, this can be achieved by exporting only the factory, while keeping each constructor private.

A mechanism to enforce encapsulation

A factory can also be used as an encapsulation mechanism, thanks to closures.

Note

Encapsulation refers to the technique of controlling the access to some internal details of an object by preventing the external code from manipulating them directly. The interaction with the object happens only through its public interface, isolating the external code from the changes in the implementation details of the object. This practice is also referred to as information hiding. Encapsulation is also a fundamental principle of object-oriented design, together with inheritance, polymorphism, and abstraction.

As we know, in JavaScript, we don't have access level modifiers (for example, we can't declare a private variable), so the only way to enforce encapsulation is through function scopes and closures. A factory makes it straightforward to enforce private variables; consider the following code for example:

function createPerson(name) { 
  const privateProperties = {}; 
 
  const person = { 
    setName: name => { 
      if(!name) throw new Error('A person must have a name'); 
      privateProperties.name = name; 
    }, 
    getName: () => { 
      return privateProperties.name; 
    } 
  }; 
 
  person.setName(name); 
  return person; 
} 

In the preceding code, we leverage closures to create two objects: a person object which represents the public interface returned by the factory, and a group of privateProperties that are inaccessible from the outside and that can be manipulated only through the interface provided by the person object. For example, in the preceding code, we make sure that a person's name is never empty; this would not be possible to enforce if name was just a property of the person object.

Note

Factories are only one of the techniques that we have for creating private members; in fact, other possible approaches are as follows:

Defining private variables in a constructor (as recommended by Douglas Crockford: http://javascript.crockford.com/private.html).

Using conventions, for example, prefixing the name of a property with an underscore "_" or the dollar sign "$" (this however, does not technically prevent a member from being accessed from the outside)

Using ES2015 WeakMaps: (http://fitzgeraldnick.com/weblog/53/).

A very complete article on this subject was published by Mozilla: https://developer.mozilla.org/en-US/Add-ons/SDK/Guides/Contributor_s_Guide/Private_Properties.

Building a simple code profiler

Now, let's work on a complete example using a factory. Let's build a simple code profiler, an object with the following properties:

  • A start() method that triggers the start of a profiling session
  • An end() method to terminate the session and log its execution time to the console

Let's start by creating a file named profiler.js, which will have the following content:

class Profiler { 
  constructor(label) { 
    this.label = label; 
    this.lastTime = null; 
  } 
 
  start() { 
    this.lastTime = process.hrtime(); 
  } 
 
  end() { 
    const diff = process.hrtime(this.lastTime); 
    console.log( 
      `Timer "${this.label}" took ${diff[0]} seconds and ${diff[1]} 
        nanoseconds.` 
    ); 
  } 
} 

There is nothing fancy in the preceding class; we simply use the default high resolution timer to save the current time when start() is invoked, and then calculate the elapsed time when end() is executed, printing the result to the console.

Now, if we are going to use such a profiler in a real-world application to calculate the execution time of the different routines, we can easily imagine the huge amount of logging we will generate to the standard output, especially in a production environment. What we might want to do instead is redirect the profiling information to another source, for example, a database, or alternatively, disable the profiler altogether if the application is running in production mode. It's clear that if we were to instantiate a Profiler object directly by using the new operator, we would need some extra logic in the client code or in the Profiler object itself in order to switch between the different logics. We can instead use a factory to abstract the creation of the Profiler object, so that, depending on whether the application runs in production or development mode, we can return a fully working Profiler object, or alternatively, a mock object with the same interface, but with empty methods. Let's do this then in the profiler.js module, instead of exporting the Profiler constructor, we will export only a function, our factory. The following is its code:

module.exports = function(label) { 
  if(process.env.NODE_ENV === 'development') { 
    return new Profiler(label);                       //[1] 
  } else if(process.env.NODE_ENV === 'production') { 
    return {                                          //[2] 
      start: function() {}, 
      end: function() {} 
    } 
  } else { 
    throw new Error('Must set NODE_ENV'); 
  } 
}; 

The factory that we created abstracts the creation of a Profiler object from its implementation:

  • If the application is running in development mode, we return a new, fully functional Profiler object
  • If instead the application is running in production mode, we return a mock object where the start() and stop() methods are empty functions

The nice feature to highlight is that, thanks to JavaScript dynamic typing, we were able to return an object instantiated with the new operator in one circumstance and a simple object literal in the other (this is also known as duck typing https://en.wikipedia.org/wiki/Duck_typing). Our factory is doing its job perfectly; we really can create objects in any way that we like inside the factory function, and we can execute additional initialization steps or return a different type of object based on particular conditions, and all of this while isolating the consumer of the object from all these details. We can easily understand the power of this simple pattern.

Now we can play with our profiler; this is a possible use case for the factory that we just created earlier:

const profiler = require('./profiler'); 
 
function getRandomArray(len) { 
  const p = profiler('Generating a ' + len + ' items long array'); 
  p.start(); 
  const arr = []; 
  for(let i = 0; i < len; i++) { 
    arr.push(Math.random()); 
  } 
  p.end(); 
} 
 
getRandomArray(1e6); 
console.log('Done'); 

The p variable contains the instance of our Profiler object, but we don't know how it's created and what its implementation is at this point in the code.

If we include the preceding code in a file named profilerTest.js, we can easily test these assumptions. To try the program with profiling enabled, run the following command:

export NODE_ENV=development; node profilerTest

The preceding command enables the real profiler and prints the profiling information to the console. If we want to try the mock profiler instead, we can run the following command:

export NODE_ENV=production; node profilerTest

The example that we just presented is just a simple application of the factory function pattern, but it clearly shows the advantages of separating an object's creation from its implementation.

Composable factory functions

Now that we have a good idea about how factory functions can be implemented in Node.js, we are ready to introduce a new advanced pattern that has recently been getting traction in the JavaScript community. We are talking about composable factory functions, which represent a particular type of factory function that can be "composed" together to build new enhanced factory functions. They are especially useful for allowing us to construct objects that "inherit" behaviors and properties from different sources without the need for building complex class hierarchies.

We can clarify this concept with a simple and effective example. Let's assume we want to build a videogame in which the characters on the screen can have a number of different behaviors: they can move on the screen; they can slash and shoot. And yes, to be a character they should have some basic properties such as life points, position on the screen, and name.

We want to define several types of character, one for every specific behavior:

  • Character: base character that has life points, a position, and a name
  • Mover: character that is able to move
  • Slasher: character that is able to slash
  • Shooter: character that is able to shoot (as long as it has bullets!)

Ideally we would be able to define new types of characters, combining different behaviors from the existing ones. We want absolute freedom and, for example, we would like to define these new types on top of the existing ones:

  • Runner: a character that can move
  • Samurai: a character that can move and slash
  • Sniper: a character that can shoot (it doesn't move)
  • Gunslinger: a character that can move and shoot
  • Western Samurai: a character that can move, slash, and shoot

As you can see, we want total freedom in combining the features of every basic type, so it should now be obvious that we cannot easily model this problem using classes and inheritance.

So instead, we are going to use composable factory functions and in particular we are going to use the stamp specification as implemented by the stampit module: (https://www.npmjs.com/package/stampit).

This module offers an intuitive interface for defining factory functions that can be composed together to build new factory functions. Basically, it allows us to define factory functions that will generate objects with a specific set of properties and methods, by using a handy fluent interface to describe them.

Let's see how easily we can define our basic types for our game. We will start with the basic character type:

const stampit = require('stampit'); 
 
const character = stampit(). 
  props({ 
    name: 'anonymous', 
    lifePoints: 100, 
    x: 0, 
    y: 0 
  }); 

In the previous snippet of code, we defined the character factory function, which can be used to create new instances of basic characters. Every character will have these properties: name, lifePoints, x, and y and the default values will be respectively anonymous, 100, 0, and 0. The props method of stampit allows us to define these properties. To use this factory function, we can do something like this:

const c = character(); 
c.name = 'John'; 
c.lifePoints = 10; 
console.log(c); // { name: 'John', lifePoints: 10, x:0, y:0 } 

Now let's define our mover factory function:

const mover = stampit() 
  .methods({ 
    move(xIncr, yIncr) { 
      this.x += xIncr; 
      this.y += yIncr; 
      console.log(`${this.name} moved to [${this.x}, ${this.y}]`);            
    } 
  }); 

In this case, we are using the methods function of stampit to declare all the methods available in the objects produced by this factory function. For our Mover definition, we have a move function that can increase the x and the y position of the instance. Notice that we can access instance properties with the keyword this from inside a method.

Now that we have understood the basic concepts, we can easily add the factory function definitions for the slasher and shooter types:

const slasher = stampit() 
  .methods({ 
    slash(direction) { 
      console.log(`${this.name} slashed to the ${direction}`); 
    } 
  }); 
 
  const shooter = stampit() 
    .props({ 
      bullets: 6 
    }) 
    .methods({ 
      shoot(direction) { 
        if (this.bullets > 0) { 
          --this.bullets; 
            console.log(`${this.name} shoot to the ${direction}`);                 
        } 
      } 
    }); 

Notice how we are using both props and methods to define our shooter factory function.

Ok, now that we have all our base types defined, we are ready to compose them to create new powerful and expressive factory functions:

const runner = stampit.compose(character, mover); 
const samurai = stampit.compose(character, mover, slasher); 
const sniper = stampit.compose(character, shooter); 
const gunslinger = stampit.compose(character, mover, shooter); 
const westernSamurai = stampit.compose(gunslinger, samurai); 

The method stampit.compose defines a new composed factory function that will produce an object based on the methods and properties of the composed factory functions. As you can tell, this is a powerful mechanism that gives us a lot of freedom and allows us to reason in terms of behaviors rather than in terms of classes.

To wrap up our example, let's instantiate and use a new westernSamurai.

const gojiro = westernSamurai();
gojiro.name = 'Gojiro Kiryu';
gojiro.move(1,0);
gojiro.slash('left');
gojiro.shoot('right');

This will produce the following output:

Yojimbo moved to [1, 0] 
Yojimbo slashed to the left 
Yojimbo shoot to the right

Note

More details about the stamp specification and the ideas behind it can be found in this post written by Eric Elliot, the original author of the specification: https://medium.com/javascript-scene/introducing-the-stamp-specification-77f8911c2fee.

In the wild

As we said, factories are very popular in Node.js. Many packages offer only a factory for creating new instances; some examples are the following:

Other modules expose both a class and a factory, but document the factory as the main method—or the most convenient way—to create new instances; some of the examples are as follows:

  • http-proxy (https://npmjs.org/package/http-proxy): This is a programmable proxying library, where new instances are created with httpProxy.createProxyServer(options)
  • The core Node.js HTTP server: This is where new instances are mostly created using http.createServer(), even though this is essentially a shortcut for new http.Server()
  • bunyan (https://npmjs.org/package/bunyan): This is a popular logging library; in its readme file the contributors propose a factory, bunyan.createLogger(), as the main method to create new instances, even though this would be equivalent to running new bunyan()

Some other modules provide a factory to wrap the creation of other components. Popular examples are through2 and from2 (we saw them in Chapter 5, Coding with Streams), which allow us to simplify the creation of new streams using a factory approach, freeing the developer from explicitly using inheritance and the new operator.

Finally, to see some packages that are using the stamp specification and composable factory functions internally, you can have a look at react-stampit (https://www.npmjs.com/package/react-stampit), which brings the power of composable factory functions to the frontend allowing you to easily compose widget behaviors and remitter (https://www.npmjs.com/package/remitter), a pub/sub module based on Redis.

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

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