Javascript applications consist of run-time objects which must be created when the application starts up.
Without DI, a programmer must manually write the object creation code. There are various strategies.
One approach is that objects be responsible for their own dependencies. For example, when an object is first created, all its dependencies are instantiated, in turn, inside its constructor function.
function Foo() {
this.dep1 = new Dep1();
this.dep2 = new Dep2();
}
let foo = new Foo();
This has the advantage of being easy to understand and implement. For small applications and prototypes, it is probably sufficient. However, the problems begin as the application gets larger.
As the number of objects in an application increases, the code rapidly becomes cluttered up with object creation code, making it more difficult to read and maintain.
Objects become tightly coupled with their dependencies. It is impossible to swap out an object for another one sharing the same interface, something you might want to do for unit tests.
Pass in dependencies from the outside
If an object creating its own dependencies doesn't work, an obvious solution is to create dependencies from outside the object and then pass them into it.
function Foo(dep1, dep2) {
this.dep1 = dep1;
this.dep2 = dep2;
}
// dep1 and dep2 created elsewhere
new Foo(dep1, dep2);
The advantage here is that the object has been decoupled from its dependencies. The code is much cleaner because the object creation code is elsewhere. We can pass anything we like to the constructor to be used as a dependency providing it implements the same interface. This gives us great flexibility.
But we have created a new problem: The dependencies of an object have dependencies themselves, and these in turn have dependencies. Every object in the application needs to have passed to it not just its own dependencies but all its transitive dependencies also. And the object at the root of our application will have to have every single object in the entire application passed to it!
Introducing DI
So obviously neither of the preceding strategies is workable at a larger scale. It turns out that the solution is to eschew 'manual' object creation altogether and delegate this task to something else. This something else is a Dependency Injection (DI) system.
As the name suggests, Dependency Injection involves the creation of objects outside of our application which are then given to (or injected into) it.
A DI system is a library that handles the task of object creation within an application. Application objects declare the dependencies that they need and the DI system will take care of creating these.
An object may declare its dependencies in different ways: through constructor parameters, with annotations (a.k.a decorators in Javascript), or by setting a static property of the constructor function.
The DI system is also able to do things like figure out transitive dependencies, enforce singletons, and check for circular dependencies.
DI is more common in the server-side world, but it is growing in popularity for front end development, most notably in the Angular framework. As front end applications become more and more complex, I predict that DI will only increase in popularity.
Diogenes: a DI implementation
To help me with my personal projects, I decided to create my own DI system. I have named it Diogenes. It can be imported from NPM and the source code is on Github.
Here is an example of it in action:
var injector = new Injector();
var Foo = function (options) {
this.dep1 = options.dep1;
this.dep2 = options.dep2;
}
Foo.inject = ['dep1', 'dep2'];
injector.register('foo', DataService, Injector.INSTANCE );
// declarations and registration of dep1 and dep2 not shown
let foo = injector.get('foo');
Foo is a POJSO with nothing unusual about it except the inject static property. This contains an array of string tokens. This array represents the declared dependencies of the Foo object. Each string token is a key to a dependency that is known by the DI system.
Dependencies are made known to the system using the register() method. This method takes three arguments. The first is our token string. The second argument is a service provider. A service provider is an object which tells the DI system how to create an object. In our example, it is simply a constructor function. The third argument is a constant which gives additional information to the DI system as to what kind of dependency we want to create. Injector.INSTANCE simply means that whenever an object requests a dependency with the 'foo' token it will be returned a new instance of DataService. There are other constants, such as Injector.CACHE_INSTANCE, which always returns the same instance of the service provider. The last thing of note is the get() method on the injector object. Whilst dependencies are automatically passed in to objects, sometimes we may wish to manually get a reference to them. The get() method allows us to do this.
Conclusion
DI is by far the best way of creating the large number of interconnected objects which make up modern Javascript applications, bringing the benefits of reduced boilerplate and better testing. It is to be hoped that in the future more and more developers will become switched on to it, and also that it will become an integral part of other MVC frameworks besides just Angular. In the meantime, please feel free to try Diogenes in your own projects and tell me what you think.