Chapter 7. Wiring Modules

The Node.js module system brilliantly fills an old gap in the JavaScript language: the lack of a native way of organizing code into different self-contained units. One of its biggest advantages is the ability to link these modules together using the require() function (as we have seen in Chapter 2, Node.js Essential Patterns), a simple yet powerful approach. However, many developers new to Node.js might find this confusing; one of the most frequently asked questions is in fact: what's the best way to pass an instance of component X into module Y?

Sometimes, this confusion results in a desperate quest for the Singleton pattern in the hope of finding a more familiar way to link our modules together. On the other hand, some might overuse the Dependency Injection pattern, leveraging it to handle any type of dependency (even stateless) without a particular reason. It should not be surprising that the art of module wiring is one of the most controversial and opinionated topics in Node.js. There are many schools of thought influencing this area, but none of them can be considered to possess the undisputed truth. Every approach, in fact, has its pros and cons and they often end up mixed together in the same application, adapted, customized, or used in disguise under other names.

In this chapter, we're going to analyze the various approaches for wiring modules and highlight their strengths and weaknesses so that we can rationally choose and mix them together depending on the balance between simplicity, reusability, and extensibility that we want to obtain. In particular, we're going to present the most important patterns related to this topic, which are as follows:

  • Hardcoded dependency
  • Dependency Injection
  • Service locator
  • Dependency Injection containers

We will then explore a closely related problem, namely, how to wire plugins. This can be considered a specialization of module wiring and it mostly presents the same traits, but the context of its application is slightly different and presents its own challenges, especially when a plugin is distributed as a separate Node.js package. We will learn the main techniques to create a plugin-capable architecture and we will then focus on how to integrate these plugins into the flow of the main application.

At the end of this chapter, the obscure art of Node.js module wiring should not be a mystery to us anymore.

Modules and dependencies

Every modern application is the result of the aggregation of several components and, as the application grows, the way we connect these components becomes a win or lose factor. It's not only a problem related to technical aspects such as extensibility, but it's also a concern with the way we perceive the system. A tangled dependency graph is a liability and it adds to the technical debt of the project; in such a situation, any change in the code aimed to either modify or extend its functionality can result in tremendous effort.

In the worst case, the components are so tightly connected together that it becomes impossible to add or change anything without refactoring or even completely rewriting entire parts of the application. This, of course, does not mean that we have to over-engineer our design starting from the very first module, but surely finding a good balance from the very beginning can make a huge difference.

Node.js provides a great tool for organizing and wiring the components of an application together: it's the CommonJS module system. However, the module system alone is not a guarantee of success; if on the one hand, it adds a convenient level of indirection between the client module and the dependency, then on the other, it might introduce a tighter coupling if not used properly. In this section, we will discuss some fundamental aspects of dependency wiring in Node.js.

The most common dependency in Node.js

In software architecture, we can consider any entity, state, or data format which influences the behavior or structure of a component as a dependency. For example, a component might use the services offered by another component, rely on a particular global state of the system, or implement a specific communication protocol in order to exchange information with other components, and so on. The concept of dependency is very broad and sometimes hard to evaluate.

In Node.js, though, we can immediately identify one essential type of dependency, which is the most common and easy to identify; of course, we are talking about the dependency between modules. Modules are the fundamental mechanism at our disposal to organize and structure our code; it's unreasonable to build a large application without relying on the module system at all. If used properly to group the various elements of an application, it can bring a lot of advantages. In fact, the properties of a module can be summed up as follows:

  • A module is more readable and understandable because (ideally) it's more focused
  • Being represented as a separate file, a module is easier to identify
  • A module can be more easily reused across different applications

A module represents the perfect level of granularity for performing information hiding and offers an effective mechanism to expose only the public interface of a component (using module.exports).

However, simply spreading the functionality of an application or a library across different modules is not enough for a successful design—it has to be done right. One of the fallacies is ending up in a situation where the relationship between our modules becomes so strong we create a unique monolithic entity, where removing or replacing a module would reverberate across most of the architecture. We are immediately able to recognize that the way we organize our code into modules and the way we connect them together play a strategic role. And, as with any problem in software design, it's a matter of finding the right balance between different measures.

Cohesion and coupling

The two most important properties to balance when building modules are cohesion and coupling. They can be applied to any type of a component or subsystem in software architecture, so we can use them as guidelines when building Node.js modules as well. These two properties can be defined as follows:

  • Cohesion: This is a measure of the correlation between the functionalities of a component. For example, a module that does only one thing, where all its parts contribute to that one single task has a high cohesion. A module that contains functions to save any type of object into a database—saveProduct(), saveInvoice(), saveUser(), and so on has a low cohesion.
  • Coupling: This measures how much a component is dependent on the other components of a system. For example, a module is tightly coupled to another module when it directly reads or modifies the data of the other module. Also, two modules that interact via a global or shared state are tightly coupled. On the other hand, two modules that communicate only via the passing of parameters are loosely coupled.

The desirable scenario is to have a high cohesion and a loose coupling, which usually results in more understandable, reusable, and extensible modules.

Stateful modules

In JavaScript, everything is an object. We don't have abstract concepts such as pure interfaces or classes; its dynamic typing already provides a natural mechanism to decouple the interface (or policy) from the implementation (or detail). That's one of the reasons why some of the design patterns that we have seen in Chapter 6, Design Patterns, looked so different and simplified compared to their traditional implementation.

In JavaScript, we have minimal problems in separating interfaces from implementations; however, by simply using the Node.js module system, we are already introducing a hardcoded relationship with one particular implementation. Under normal conditions, there is nothing wrong with this, but if we use require() to load a module that exports a stateful instance, such as a db handle, an HTTP server instance, the instance of a service, or in general any object which is not stateless, we are actually referencing something very similar to a Singleton, thus inheriting its pros and cons, with the addition of some caveats.

The Singleton pattern in Node.js

A lot of people new to Node.js get confused about how to implement the Singleton pattern correctly, most of the time with the simple intent of sharing an instance across the various modules of an application. However, the answer in Node.js is easier than what we might think; simply exporting an instance using module.exports is already enough to obtain something very similar to the Singleton pattern. Consider, for example, the following line of code:

//'db.js' module 
module.exports = new Database('my-app-db'); 

By simply exporting a new instance of our database, we can already assume that within the current package (which can easily be the entire code of our application), we are going to have only one instance of the db module. This is possible because, as we know, Node.js will cache the module after the first invocation of require(), making sure to not execute it again at any subsequent invocation, returning instead the cached instance. For example, we can easily obtain a shared instance of the db module that we defined earlier, with the following line of code:

const db = require('./db'); 

But there is a caveat; the module is cached using its full path as lookup key, therefore it is guaranteed to be a Singleton only within the current package. We saw in Chapter 2, Node.js Essential Patterns, that each package might have its own set of private dependencies inside its node_modules directory, which might result in multiple instances of the same package and therefore of the same module, with the result that our Singleton might not be single anymore. Consider, for example, the case where the db module is wrapped into a package named mydb. The following lines of code will be in its package.json file:

{ 
  "name": "mydb", 
  "main": "db.js" 
} 

Now consider the following package dependency tree:

app/ 
`-- node_modules 
    |-- packageA 
    |  `-- node_modules 
    |      `-- mydb 
    `-- packageB 
        `-- node_modules 
            `-- mydb 

Both packageA and packageB have a dependency on the mydb package; in turn, the app package, which is our main application, depends on packageA and packageB. The scenario we just described will break the assumption about the uniqueness of the database instance; in fact, both packageA and packageB will load the database instance using a command such as the following:

const db = require('mydb'); 

However, packageA and packageB will actually load two different instances of our pretending Singleton because the mydb module will resolve to a different directory depending on the package it is required from.

At this point, we can easily say that the Singleton pattern, as described in the literature, does not exist in Node.js, unless we don't use a real global variable to store it, something such as the following:

global.db = new Database('my-app-db'); 

This would guarantee that the instance will be the only one and shared across the entire application, not just the same package. However, this is a practice to avoid at all costs; most of the time, we don't really need a pure Singleton, and anyway, as we will see later, there are other patterns that we can use to share an instance across the different packages.

Note

Throughout this book, for simplicity, we will use the term Singleton to describe a stateful object exported by a module, even if this doesn't represent a real Singleton in the strict definition of the term. We can surely say, though, that it shares the same practical intent with the original pattern: to easily share a state across different components.

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

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