A power on lightbulbA power off lightbulb

Why you probably want dependency injection on node.js

published on Ramiel's Creations



Tuesday, May 10, 2016

Premise

Recently, while we were developing an API in node, some of my colleagues come to me saying that the dependency injection pattern I was using until that moment wasn't useful on our project. They give me a lot of reasons then. Before going to talk about those reasons maybe it worth to give some background.

Dependency injection

I don't want to explain what dependency injection is or this article will became too long so I let someone better than me to do it: you can give a look at this article by Fabien Potencier. It talks about DI in PHP and then, more specifically on symfony but is a very good reading. Of course you can find plenty of article which explain the same mechanism on javascript. I personally find very well written the chapter on the book Node.js design Patterns by Mario Casciaro.

Dependency injection is the starting point for a more complicated inversion of control scenario but it is a good starting point (and a simple one) to gain a lot in your code, without much effort.

The arguments

Here a list of arguments I received why I should stop doing DI and I should start something different. I report here the arguments in a non particular order.

  1. In javascript you don't need DI because the language itself permit you to change anything (they refer to any object) without any particular problem.

Well, even if you can do something this doesn't mean you should.

The reason behind this argument come from the fact that DI help you to have a more testable code and in javascript you don't need DI to obtain this result.

Let's take this code which use DI.

class UserBusiness{ constructor(userModel){ this.userModel = userModel; } buildAndSave(cb){ /* stuff... */ this.userModel.save(this, cb); } } // Somewhere else const UserModel = require('UserModel'); let b = new UserBusiness(new UserModel());

So in this case we have a business object which use a usermodel to persist data on a DB. With DI you can easily write unit tests for this business class injecting a mock for the userModel, so in the test module you'll have something similar:

var userModelInstance = require('userModelMock'); b = new UserBusiness(userModelInstance);

In the mock you'll have a fake save method which do nothing, just call the passed callback (we can complicate it but let's stay it simple like this).

Now let's examine the same example without DI

const UserModel = require('UserModel'); class UserBusiness{ constructor(){ this.userModel = new UserModel(); } buildAndSave(cb){ /* stuff... */ this.userModel.save(this, cb); } }

What is you want to write an unit test for this business class? Here what they suggest:

const UserModel = require('UserModel'); UserModel.prototype.save = function(data, cb){ cb(); } b = new UserBusiness();

So we're using this concept "in javascript you can modify any object". The problem is that you have to do it in any test when you need to mock the model. Even if this works we must notice two important things:

  • we need to know how UserModel is coded and so we have to hope it have a simply modifiable code like in the former example
  • we are relying on the particular implementation of require which is a bad practice.

You can understand why the first reason can be a problem. I'll explain why rely on the require implementation can be a problem soon.

note: you can obtain the same result using libraries like proxyquire, the result is not far from this

  1. If you're using DI to have always the same instance of something on all your application you can use a singleton instead

This arguments comes from the fact that this people thinks I'm using DI to just share the same instance of something across all my application, which is partially false.

Let's say we have a module which handle the connection to the DB and it is the only entrypoint to have a DB connection. Probably I'll inject an instance of this component to all others components which needs an access to the DB. Then, the fact that I need just one instance is not a natural consequence but let's agree with my folks and let's pretend it.

They're solution is the following: instead of inject an instance to any other components, just create a file which exports an instance and use it to obtain a singleton.

This leads to two problems:

  • Your project is wasted of files which exports a singleton
  • You're still relying on require implementation to obtain a singleton.

This looks very bad. The smarter among you (so everybody) have already noticed that is actually not needed to create an additional file while you can simply expose a function which give you a singleton. Here is how you can use such implementation:

const DbComponent = require('DbComponent'); const dbComponentInstance = DbComponent.getSingleton(); /* any code you want... */

The problem about this is that is hard to test this connection component with different configurations. You'll always have to mock the getSingleton function to change the instance based on a different configuration.

You're even creating a singleton when is not needed at all, just to solve a problem you created!

  1. Require is actually an implementation of a dependency container

Mario Casciaro explains very well why this is potentially false and which problem you can find but let see this specific one

var Server = function(logger){ this.logger = logger this.logger.debug('Instantiated server'); }

In this simple constructor I'm using DI to inject the logger. The code without DI looks like this:

const loggerConfig = require('loggerConfig'); const logger = new Logger(loggerConfig); var Server = function(){ logger.debug('Instantiated server'); }

I had then the needs to launch the server with a different log level based on a parameter passed from the command line (let's pretend this ok?). So:

var loggerConfig = require('loggerConfig'); loggerConfig.level = isVerbose ? 'debug' : 'error'; const Server = require('Server'); new Server();

The problem here is

  • What happens if you put the line 3 above the line 2?

This code won't work anymore because the Logger implementation do not react to a later change of the configuration passed to its constructor (it's like this for real for example with Bunyan). This code is far more manageable with the DI version where the instantiations becomes like this:

var loggerConfig = require('loggerConfig'); loggerConfig.level = isVerbose ? 'debug' : 'error'; const Logger = require('Logger'); const logger = new Logger(loggerConfig); const Server = require('Server'); new Server(logger);

The requiring order becomes natural because DI force you to think about which component is needed to create another one.

Then we finally come to why it's a bad idea to rely on the require implementation (others than what already explained). You can use require as a dependency container or a singleton provider because internally require implement a cache system. Even if this is unlikely to change you should not rely on this fact. Then, that cache system is based on the absolute path of a module which leads to several problems when the same module is provided by different submodule (and so potentially stored under different absolute paths).

  1. DI is not elegant

Ok, I should not answer to this, really. By the way even if DI is not elegant it apports so many advanteges taht maybe we can stop arguing on what is elegant and what not!

In any case I showed you here a very simple implementation of DI which probably is called contructor injection. There are a lot of ways to make it more complicated (dependency resolution), maybe more elegant (dependency injection containers) and other patterns. Just remembers that those patterns are decoration over the simply DI which is always the base to obtain testability and separation of your code

Conclusions

It's DI going to solve all of my problems? Of course not. Just remember about it because probably it solves some problems you may find and you really do not need to reinvent the wheel.

For the love of truth I have to say I love arguing with my friends about this subjects so I have to say thanks to them afterall!