Moduling system introduced with Node.js solved that major problem. The system is called CommonJs and thanks to it, component name collision is a thing of the past and each module has its own scope which is a safe haven for its components.
However, even with that problem solved, wiring modules still must be done with care. The system you setup for wiring your components together can make or break the project. While there’s no need for over-engineering from the get go, it certainly pays off to know how to properly wire your modules.
Cohesiveness and Coupling
Let’s first go over the basics.
Cohesiveness measures how well your module and its components are geared towards doing one thing well. For example, if you have a module dedicated solely to identifying and filtering spam messages, that module probably has high cohesiveness. On the other hand, if you have a module which exposes methods for modifying different kinds of database objects like
post and etc., then that module will have low cohesiveness.
Coupling measures how much component is dependent on the other components. If the module directly accesses and modifies the data of the other module or some global variable, then that module is tightly coupled and will be harder to make changes to. If the only way for two modules to communicate is by exchanging values through their function parameters, then those modules are loosely coupled.
As a rule of thumb, in order to create scalable and maintainable system, you want to aim for high cohesiveness and loose coupling.
Stateful & Stateless modules
In Node.js modules can be stateless or stateful:
- Stateful – a module that exposes an instance of some stateful object (db connection, third party service instance, socket and etc).
- Stateless – a module that exposes stateless entities like classes or utility methods.
When working with stateful module you should have a Singleton pattern to make sure all the other modules that require the stateful module access the same instance. With Node.js you don’t have to worry about explicitly wrapping your stateful module in a Singleton pattern thanks to the mechanics of the CommonJs system.
The very first time you require your module, Node.js will cache that instance and on the subsequent requires will serve the same instance. The only time this would fail is if you require an instance that is outside of current package. The reason is because when caching the instance, Node.js uses that instance’s package address as a key.
It’s far more important to be careful when working with stateful modules than the stateless ones. While having tight coupling within the stateless modules can be bad for your architecture, the repercussions of poor wiring are far less damaging than the ones for the stateful instance.
The first thing that might come to mind when thinking about Node.js dependencies is the content of
node_modules folder. However, any component or piece of data needed in order for your module to work correctly qualifies as a dependency. Dependency can be anything from database connection instance, to a simple string with the file path.
You might have heard terms “Hardcoded dependency” and “Dependency injection” and wondered about the difference. While it might sound a bit vague, the difference is pretty simple:
- Hardcoded dependency refers to the module wiring pattern where you hardcode the name of the dependency inside of your module, possibly using
- Dependency injection is a wiring pattern where the dependencies are not hardcoded but provided as the input by the external entity instead.
To better illustrate the difference let’s build a simple stateful module for a movie suggestion using the two methods:
Let’s start off with the module for exposing database connection:
In this module we initiate a simple database connection instance and export it.
Next, our movie suggestion module where we will specify our database connection dependency by hardcoding it. For the brevity of the example I left out the implementation of the methods:
Now let’s take a look at the
app module which will act as the main entry point and start the server:
The example above is how developers would normally wire their dependencies – by hardcoding them into the module. Here are some pros and cons of using this approach:
- Intuitive and easy to understand – you can easily understand and follow the workflow of the program because all the dependencies are statically injected.
- Easier to debug.
- Modules are less flexible and are tightly coupled with their dependencies – in our example you can’t use different database instance without making the direct change to the movies module.
- Harder to unit test – since our dependencies are hardcoded it’s harder to provide dummy data for unit tests.
Hardcoding your dependencies is acceptable and totally valid most of the time and there might be no need to over-engineer it. However, we can do better in terms of flexibility and reusability by using dependency injections.
In this example we will refactor our movie suggestion service to use dependency injection instead. Let’s start off with our db connection module:
Now our module exposes a factory function instead of the instance. As you can see all the hardcoded data needed for setting up the connection is provided by the outside entity through the
settings parameter. Next, let’s refactor our movie suggestion module:
Our main dependency – the database connection is also provided by the outside entity. Now this module provides a factory function for a movie suggestion service. Finally, let’s take a look at
app module that is responsible for properly wiring all the dependencies together:
As you can see we shifted the responsibility of setting up the dependencies from the component modules to the app module. This is one of the main concepts in dependency injection: move handling the dependencies to the higher level components, which will be responsible for distributing them to the lower-level components. The reasoning behind this is that higher level components are less likely to be reused or changed and are better adapted to tight coupling.
Pros and cons of this approach:
- Higher reusability – In our case it’s much easier to use our movie suggestion service with different database instances.
- Easier unit testing – Supplying dummy data for unit testing becomes trivial.
- Dependencies are wired at a runtime – since our dependencies are wired to the components at the runtime it’s harder to understand the flow of the logic just by looking at the components. In our example not much complexity is added, but the bigger your app becomes the harder it will be to manage those dependencies.
Types of dependency injections
In the previous example we used one variation of dependency injection – factory injection. There are two more variations:
- Constructor injection – when you define the dependency during the object initialization
- Property injection – not as robust as the first two, but this variation might be necessary sometimes when there’s a cycled dependency between the modules.
Wiring modules is an important aspect of building Node.js application and it pays off to spend some time planning it. Try aiming for a high cohesion and loose coupling between the modules, especially when working with stateful modules.
In this post we discussed two methods for module wiring: hardcoding the dependencies and using dependency injection. Both of the methods have their strengths and weaknesses and it’s important to be aware of them and use them correctly based on the complexity and needs of your project.
If you’d like to get more web development, React and TypeScript tips consider
following me on Twitter,
where I share things as I learn them.