published on Ramiel's Creations
Tuesday, May 10, 2016
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.
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.
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.
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:
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
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:
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!
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
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).
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
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!