Dependency Injection Explained via JavaScript
Understand dependency injection by implementing a simple constructor-based framework for managing inversion of control.
When learning a new framework I often find it is useful to examine the source, use the framework, then go into a separate project and build the functionality from scratch to better understand the motivation behind the framework and what it may be saving me by using it. Angular is no exception. There are many tools in the AngularJS toolbox, from data-binding to compiling new HTML tags, but one of my favorites is the built-in dependency injection. You can browse Angularâs DI code here and read my blog posts about understanding Providers, Services, and Factories. A more advanced version is detailed in Interception using Decorator and Lazy Loading with AngularJS.
âWell, Dimitri, every search for a hero must begin with something every hero needs, a villain. So in a search for our hero, Bellerophon, we have created a more effective monster: Chimera.â â Dr. Nekhorvich, Mission Impossible II
I donât believe I created a monster, but in my search to understand JavaScript dependency injection, I did create jsInject.
This library acts as an dependency injection container for JavaScript components that depend on each other. It is based on constructor injection which inverts control of dependencies by passing them in through the constructor. If you are looking for dependency injection in JavaScript without taking on a full framework, this is an extremely lightweight solution that should do the trick for you.
For example, if your serviceA depends on dependencyB, you might define it like this to return it from a factory:
function serviceA(dependencyB) {
return {
id: dependencyB.getId()
};
}
Another approach is to use a constructor function. This is required if you use a class-based approach, as code generated from tools like TypeScript doesnât lend itself well to the factory pattern.
function ServiceA(dependencyB) {
this.id = dependencyB.getId();
}
Of course there is always the self-invoking function as well:
var ServiceA = (function() {
function ServiceA(dependencyB) {
this.id = dependencyB.getId();
}
return ServiceA;
})();
In all of these cases, the dependency is injected and the purpose of a dependency injection container is to handle that injection for you.
Why Dependency Injection
A logical first question is: why bother? I often hear this from developers and architects who are concerned that dependency injection adds unnecessary overhead and over-complicates projects. More often than not they have worked on smaller projects with smaller teams and might not have run into the sheer size of project that benefits from dependency injection. Iâm used to projects where there are tens of thousands if not hundreds of thousands of lines of client code (yes, Iâm talking just the JavaScript part) with hundreds of components that interrelate. Forget module loading, bundling, etc. for a moment (worrying about how the scripts get loaded in the first place), letâs take a look at common problems:
- When A depends on B and B depends on C, without dependency injection you create C from B and B from A and then reference A from a dozen different places. What happens when C now requires something, or B requires D in addition to C? Without dependency injection, that is a lot of refactoring and finding places you create those components. With dependency injection, A only has to worry about B. A doesnât care what B depends on because that is handled by the DI container.
- Timing is often an issue. What happens when A depends on B but the script for B is loaded after A? Dependency injection removes that concern because you can defer or lazy load the dependency when itâs time. In other words, regardless of how A and B are loaded, you only need to wire them up when you first reference A. On large projects when you have 50 JavaScript components referenced from a single page, the last thing you want to have to worry about is including them in the correct order.
- Dependency injection is critical for testing. If A directly instantiates B then Iâm stuck with the implementation of B that A chooses. If B is injected into A, I can create a âmock Bâ or a âstub Bâ for testing purposes. For example, you might have a component that depends on a web service. With dependency injection, you can create an implementation that uses hard-coded JSON for testing purposes and then wire in the real component that makes the service call at runtime.
- Single Responsibility â Dependency Injection is actually one of the SOLID principles. It helps to encourage the notion of designing components with a Single Responsibility. You want to be able to focus on one thing in a component an should only have to change it for one reason. This is like building padding around your component, insulting the rest of the system. If the component is responsible for boiling water and mixing shakes, anytime you change it you have to test both scenarios. Having a single responsibility means you can change how you boil water without worrying about making shakes because another component is responsible. That makes it easier to maintain the code.
- Psst ⊠managers. Hereâs something else to think about. Forget the technology, letâs talk about teams. When you have a lot of cooks in the kitchen, it is easy for them to bump elbows and step on each otherâs toes. Having nice, isolated components means more team members can work in parallel without having to worry about what the other person is doing. Iâve witnessed this on many projects â again, if you have one component boil water and mix shakes, only one person can apply changes at a time. If you have two separate components, you can improve both scenarios at the same time.
I believe there are many benefits and that DI itself is only as complicated as you make it. In my experience, it simplifies things on larger projects.
Dependency Injection to the Rescue
The most basic DI solutions provide at least two features: the ability to register a component, and the ability to retrieve an instance of a component. Advanced solutions will allow you to intercept requests, override methods on the fly, register multiple components that satisfy a given interface and even manage the lifetime of a component (whether you get a new instance or the same one each time). The registration is key because that tells the container what you expect to retrieve, and somehow the container must also understand what dependencies to provide.
A naĂŻve implementation will use the constructor parameters to determine dependencies. I call this naĂŻve because naming a parameter doesnât imply the dependent component has the same name, and when you minify or uglify your JavaScript code you lose information. A more common approach is to annotate the component somehow. I really like the options that Angular provides for annotations. When you register a component, you give it a name that doesnât necessarily have to match the name of the constructor function or the function being called as a factory. When you annotate a component, you simply provide a list of names of dependencies. There are two ways to do this. You can either mark these up on the component itself by exposing a property as an array, or you can pass these in to the container when you register it.
Registering the Component
Letâs register a component. For jsInject I decided to mirror Angularâs approach and allow you to either pass dependencies at registration time, or use a static property. Here is an example of providing the information at registration time:
var fn = (function() {
function Fn(echo) {
this.echo = echo;
this.test = function() {
return echo.echo(expected);
};
}
return Fn;
})();
$jsInject.register("1", ["echoFn", fn]);
In this case the dependency passed in as âechoâ to the constructor is registered as âechoFnâ to the container, and the function we are registering called “fn” is labeled â1â in the container. The registration expects an array. The last member should always be the component we are registering (whether it is a constructor function or factory) and the members before it are the names of the dependencies in the order they will be passed in.
The other approach is to annotate the component. Here is an example of using the annotation approach. This service exposes a hard-coded static property called $$deps to list the dependency names:
function ServiceB(serviceA) {
this.id = serviceA.id + "b";
}
ServiceB.$$deps = ["ServiceA"];
It is registered like this (notice there are no âannotationsâ added to the registration array, just the constructor function itself):
$jsInject.register("ServiceA", [ServiceA]);
jsInject creates a method for instantiating the component but does not try to invoke it right away. It is a lazy-loading function. The first time the component is needed, it will instantiate it, then it will return the same copy for future requests. Here is the general pattern â weâll expand the magic part, but note how once itâs done, it wonât go through that work again because the function replaces itself with a new one that simply returns the instantiated component.
var _this = this;
this.container[name] = function (level) {
var result = {}; // magic goes here
_this.container[name] = function () {
return result;
};
return result;
};
So how exactly does it wire up the dependencies?
Retrieving the Component
The magic is in walking through the dependency chain. The algorithm itself is fairly simple. You basically iterate the list of annotations, grab them from the container recursively, and shove them into a list of arguments that youâll pass to the componentâs constructor. If an annotation has been instantiated, it is returned, otherwise it is also scanned for its own annotations and so forth. The trick is understanding the format of what was passed in and generically instantiating it with a dynamic constructor list.
The level keeps track of recursion to avoid infinite loops. You can tweak this as needed but Iâve seldom seen well-written systems go more than a few levels deep (for example, a controller may depend on a service that depends on other services). The dynamic nature of JavaScript makes it easier for us to build a component on the fly. First, we create an empty template:
Template = function () {}
Next, we get the component itself (which is a function, but it might be a constructor function, a factory, or a self-invoking function):
fn = annotatedArray[annotatedArray.length - 1],
Then we grab the annotations for the component. Remember, the last element of the array is the component itself. If the array has more than one element, we assume the previous elements are annotations. Otherwise, we check the component itself for the $$deps property, and failing that we assume it has no dependencies.
deps = annotatedArray.length === 1 ? (annotatedArray[0].$$deps || []) :
annotatedArray.slice(0, annotatedArray.length - 1),
Now for the magic, we copy the prototype of the supplied component to the prototype of our template:
Template.prototype = fn.prototype;
Then we create an instance of the template:
instance = new Template();
Finally, we call a recursively invoked function to push in dependencies. If there are none, we use the instance we created, otherwise we use the fully wired result from the recursive call.
injected = _this.invoke(fn, deps, instance, lvl + 1);
result = injected || instance;
The recursive call simply checks the recursion level to make sure we havenât gone too far, then iterates through the dependencies. Each dependency is pushed into an arguments list (as an instance that is also recursively retrieved from the container itself). We use the function to apply to the template we created with the arguments weâve pushed.
for (; i < deps.length; i += 1) {
args.push(this.get(deps[i], lvl + 1));
}
return fn.apply(instance, args);
Voila! Now we are able to retrieve a component with its dependencies. Here is an example of wiring up multiple components, some of them by annotating during registration and others annotated using the $$deps:
function main () {
var ioc = new $$jsInject();
ioc.register('courseMap', [CourseMap]);
ioc.register('instructors', ['log', 'ch', Instructors]);
ioc.register('courses', ['log', 'ch', Courses]);
ioc.register('log', [Log]);
ioc.register('ch', [CollectionHelper]);
ioc.get('log').log(ioc.get('courseMap').getCourses());
}
You can see a full working example with this fiddle. Be sure to open your console log, that is where you can verify the call to the course map returns an expanded instructor and course but all dependent components are only created once even though they were registered in a different order.
Improvements
You can certainly improve the implementation (and I do accept pull requests). As an exercise, for example, how would you modify it so you could store constant values and not just instances of objects? In other words, what if I want to register the text âCopyright 2014â in a value called âCopyrightâ? Also, the current implementation for registration doesnât implement chaining. Instead of ioc.register(x); ioc.register(y)
it would be far more convenient to chain like this: ioc.register(x).register(y)
⊠do you know how to fix it so thatâs possible? What about a modification that allows you to determine whether you get the singleton, cached instance vs. force a âfreshâ instance when you request it? (Hint: take a look at my Portable IOC project that supports this in C#).
Wrapping Up
As you can see, dependency injection itself is fairly straightforward and is designed to simplify your solution, not overly complicate it. Angular adds some extra caveats and shortcuts to their solution but in essence it behaves very similar to the example Iâve provided here. In fact, I hope by walking through this you can now go back to the Angular source and better understand what is happening in the $inject implementation. If you are able to use the jsInject component in your projects thatâs an added bonus, but otherwise I hope you walk away feeling much better about using dependency injection and specifically how it can be implemented in JavaScript.
Regards,
Related articles: