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.
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.
Let's take this code which use DI.
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:
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
What is you want to write an unit test for this business class? Here what they suggest:
requirewhich 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:
requireimplementation 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:
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
In this simple constructor I'm using DI to inject the logger. The code without DI looks like this:
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:
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:
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!