This article is meant to demystify those scary terms DI and IoC. We are going to code this in a node environment.
Imagine having the following code
// index.jsclassDatabase{insert(table,attributes){// inserts record in database// ...constisSuccessful=truereturnisSuccessful}}classUserService{create(user){// do a lot of validation etc.// ...constdb=newDatabasereturndb.insert('users',user)}}constuserService=newUserServiceconstresult=userService.create({id:1})console.log(result)
Running node index.js should now log the value "true".
What is happening in the code? There is a Database class used to save things into the database and a UserService class used to create users. The users are going to be saved in the database, so when we create a new user, we new up an instance of Database. In other words, UserService is dependent on Database. Or, Database is a dependency of UserService.
And here comes the problem. What if we were to write tests to check the part // do a lot of validation etc.. We need to write a total of 10 tests for various scenarios. In all of these tests, do we really want to insert users into the database? I don't think so. We don't even care about this part of the code. So it would be nice if it was possible to swap out the database with a fake one when running tests.
Dependency Injection
Enter dependency injection. It sounds very fancy, but in reality is super simple. Rather than newing up the Database instance inside the "create" method, we inject it into the UserService like this.
classDatabase{insert(table,attributes){// inserts record in databaseconstisSuccessful=truereturnisSuccessful}}classUserService{constructor(db){this.db=db}create(user){returnthis.db.insert('users',user)}}constdb=newDatabaseconstuserService=newUserService(db)constresult=userService.create({id:1})console.log(result)
But of course, I hear what you saying. While we made the code testable, the API suffered from it. It's annoying to always pass in an instance of Database.
Inversion of Control
Enter Inversion of Control. Its job is to resolve dependencies for you.
It looks like this: At the start of the app you bind the instantiation to a key and use that later at any point.
Before we check out the code of our IoC container (also called service container), let's look at the usage first.
Now you can use ioc.use at any point in your app to access the userService.
ioc.use('userService').create({id:1})
Whenever you call ioc.use('userService'), it will create a new instance of UserService, basically executing the callback of the second function. If you prefer to always access the same instance, use app.singleton instead of app.bind.
global.ioc={container:newMap,bind(key,callback){this.container.set(key,{callback,singleton:false})},singleton(key,callback){this.container.set(key,{callback,singleton:true})},use(key){constitem=this.container.get(key)if (!item){thrownewError('item not in ioc container')}if (item.singleton&&!item.instance){item.instance=item.callback()}returnitem.singleton?item.instance:item.callback()},}
That's not a lot of code at all!
so the methods bind and singleton just store the key and callback inside a map and with the use method, we get what we want from the container again.
We also make ioc a global variable so it is accessible from anywhere.
But where do we put all those ioc bindings?
Service Providers
Enter the service provider. Another fancy term simply meaning "This is where we bind our stuff in the service container". This can be as simple as having
This works, but there is the problem that if you have tests that require the actual database in the userService, these might also receive the TeastableDatabase now. Let's create a fake and restore method on the ioc object instead. We also have to alter our use method a little
global.ioc={container:newMap,fakes:newMap,bind(key,callback){this.container.set(key,{callback,singleton:false})},singleton(key,callback){this.container.set(key,{callback,singleton:true})},fake(key,callback){constitem=this.container.get(key)if (!item){thrownewError('item not in ioc container')}this.fakes.set(key,{callback,singleton:item.singleton})},restore(key){this.fakes.delete(key)},use(key){letitem=this.container.get(key)if (!item){thrownewError('item not in ioc container')}if (this.fakes.has(key)){item=this.fakes.get(key)}if (item.singleton&&!item.instance){item.instance=item.callback()}returnitem.singleton?item.instance:item.callback()},}
With the IoC container this abstraction is not necessary, thus making the code base cleaner.
Avoids relative require
Imagine you are somewhere very deep inside the file app/controllers/auth/UserController.js and want to require the file app/apis/GitHub.js. How do you do that normally?
constGitHub=require('../../apis/GitHub')
How about we add this to the service container instead?
But with that the ioc container must live in the root of the directory. Let's extract the IoC container out and make a factory out of it. The end result is
//lib/ioc.jsmodule.exports=functioncreateIoC(rootPath){return{container:newMap,fakes:newMap,bind(key,callback){this.container.set(key,{callback,singleton:false})},singleton(key,callback){this.container.set(key,{callback,singleton:true})},fake(key,callback){constitem=this.container.get(key)if (!item){thrownewError('item not in ioc container')}this.fakes.set(key,{callback,singleton:item.singleton})},restore(key){this.fakes.delete(key)},use(namespace){letitem=this.container.get(namespace)if (item){if (this.fakes.has(namespace)){item=this.fakes.get(namespace)}if (item.singleton&&!item.instance){item.instance=item.callback()}returnitem.singleton?item.instance:item.callback()}returnrequire(path.join(rootPath,namespace))}}}
We wrapped the object inside the function createIoC that expects the root path to be passed in. The "require" method now returns the following return require(rootPath + '/' + path).
And inside index.js we now have to create the container like this
global.ioc=require('./lib/ioc')(__dirname)
And that's it for the basics of IoC! I put the code on GitHub where you can check it out again. I also added some tests to it and made it possible to fake root requires as well.