Dependency Injection in JavaScript

I like the quote that goes something like, “Programming is all about managing complexity.” The computer world is a giant construction of abstractions. We simply wrap things and produce new tools over and over again. Just think for a minute. The languages which you use have built-in functionality that are probably abstracted functions of other low level operations. It’s the same with JavaScript.

Sooner or later you need to use abstractions made by other developers – i.e. you depend on someone’s other code. I like dependency-free modules, but that’s difficult to achieve. Even if you create those nice black-box-like components you still have a part which combines everything. That’s where the dependency injection comes in. The ability to manage the dependencies effectively is absolutely necessary nowadays. This articles sums up my observations on the problem and some of the solutions.

The Goal

Let’s say that we have two modules. The first one is a service which makes Ajax requests and the second one is a router.

var service = function() {
    return { name: 'Service' };
}
var router = function() {
    return { name: 'Router' };
}

We have another function that needs these modules.

var doSomething = function(other) {
    var s = service();
    var r = router();
};

To make the things a little bit more interesting, the function needs to accept one more parameter. Sure, we could use the above code, but that’s not really flexible. What if we want to use ServiceXML or ServiceJSON. Or what if we want to mockup some of the modules for testing purposes. We can’t just edit the body of the function. The first thing which we all come up with is to pass the dependencies as parameters to the function. I.e.:

var doSomething = function(service, router, other) {
    var s = service();
    var r = router();
};

By doing this we are passing the exact implementation of the module which we want. However this brings a new problem. Imagine if we have doSomething all over our code. What will happen if we need a third dependency. We can’t edit all the function’s calls.

What we need is an instrument which will do that for us. That’s what dependency injectors are trying to solve. Let’s write down few goals that our dependency injection solution should achieve:

  • We should be able to register dependencies;
  • The injector should accept a function and should return a function that somehow gets the needed resources;
  • We should not write a lot – we need short and nice syntax;
  • The injector should keep the scope of the passed function;
  • The passed function should be able to accept custom arguments, not only the described dependencies;

A nice list isn’t it. Let’s dive in.

The RequireJS / AMD approach

You probably already know about RequireJS. It’s a nice option for solving dependencies.

define(['service', 'router'], function(service, router) {       
    // ...
});

The idea is firstly to describe the needed dependencies and then write your function. The order of the arguments is, of course, important here. Let’s say that we write a module called injector which will accept the same syntax.

var doSomething = injector.resolve(['service', 'router'], function(service, router, other) {
    expect(service().name).to.be('Service');
    expect(router().name).to.be('Router');
    expect(other).to.be('Other');
});
doSomething("Other");

Before to continue I should clarify the body of the doSomething function. I’m using expect.js as a assertion library just to be sure that the code which I’m writing works as I want. A little bit TDD approach.

Here is how our injector module starts. It’s good for it to be a singleton, so it does its job from different parts of our application.

var injector = {
    dependencies: {},
    register: function(key, value) {
        this.dependencies[key] = value;
    },
    resolve: function(deps, func, scope) {

    }
}

This is a really simple object which has two functions and one variable that acts as a storage. What we have to do is to check the deps array and search for answers in the dependencies variable. The rest is just calling the .apply method against the past func parameter.

resolve: function(deps, func, scope) {
    var args = [];
    for(var i=0; i<deps.length, d=deps[i]; i++) {
        if(this.dependencies[d]) {
            args.push(this.dependencies[d]);
        } else {
            throw new Error('Can't resolve ' + d);
        }
    }
    return function() {
        func.apply(scope || {}, args.concat(Array.prototype.slice.call(arguments, 0)));
    }        
}

If there is any scope it is effectively used. Array.prototype.slice.call(arguments, 0) is necessary to transform the arguments variable to an actual array. So far so good. Our test passes. The problem with this implementation is that we have to write the needed components twice and we can’t really mix their order. The additional custom parameters are always after the dependencies.

The Reflection Approach

According to Wikipedia reflection is the ability of a program to examine and modify the structure and behavior of an object at runtime. In simple words and in the context of JavaScript, that’s reading the source code of an object or function and analyzing it. Let’s get our doSomething function from the beginning of the article. If you log doSomething.toString() you will get the following string:

"function (service, router, other) {
    var s = service();
    var r = router();
}"

Having the method as a string gives us the ability to fetch the expected parameters and, more importantly, their names. That’s what Angular uses for its dependency injection implementation. I cheated a bit and got the regular expression which exports the arguments directly from the Angular’s code.

/^functions*[^(]*(s*([^)]*))/m

We could change the resolve class to the following:

resolve: function() {
    var func, deps, scope, args = [], self = this;
    func = arguments[0];
    deps = func.toString().match(/^functions*[^(]*(s*([^)]*))/m)[1].replace(/ /g, '').split(',');
    scope = arguments[1] || {};
    return function() {
        var a = Array.prototype.slice.call(arguments, 0);
        for(var i=0; i<deps.length; i++) {
            var d = deps[i];
            args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift());
        }
        func.apply(scope || {}, args);
    }        
}

We run the RegExp against the function’s definition. The result is:

["function (service, router, other)", "service, router, other"]

So, we need only the second item. Once we clean up the empty spaces and split the string we got the deps array filled. There is only one more change:

var a = Array.prototype.slice.call(arguments, 0);
...
args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift());

We are looping through the dependencies and, if there is something missing, trying to fetch it from the arguments object. Thankfully the shift method returns simply undefined if the array is empty rather than throwing an error. The new version of the injector could be used like this:

var doSomething = injector.resolve(function(service, other, router) {
    expect(service().name).to.be('Service');
    expect(router().name).to.be('Router');
    expect(other).to.be('Other');
});
doSomething("Other");

There is no need to rewrite the dependencies and their order can be mixed. It still works and we replicated the Angular’s magic.

However, the world is not perfect and there is one very big problem with this reflection type of injection. Minification will break our logic because it changes the names of the parameters and we will no longerbe able to resolve the dependencies. For example, a minified doSomething() might look like this:

var doSomething=function(e,t,n){var r=e();var i=t()}

The solution proposed by Angular’s team looks like that:

var doSomething = injector.resolve(['service', 'router', function(service, router) {

}]);

This looks like the solution which we started with. I personally wasn’t able to find a better solution, and decided to mix the two approaches. Here is the final version of the injector.

var injector = {
    dependencies: {},
    register: function(key, value) {
        this.dependencies[key] = value;
    },
    resolve: function() {
        var func, deps, scope, args = [], self = this;
        if(typeof arguments[0] === 'string') {
            func = arguments[1];
            deps = arguments[0].replace(/ /g, '').split(',');
            scope = arguments[2] || {};
        } else {
            func = arguments[0];
            deps = func.toString().match(/^functions*[^(]*(s*([^)]*))/m)[1].replace(/ /g, '').split(',');
            scope = arguments[1] || {};
        }
        return function() {
            var a = Array.prototype.slice.call(arguments, 0);
            for(var i=0; i<deps.length; i++) {
                var d = deps[i];
                args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift());
            }
            func.apply(scope || {}, args);
        }        
    }
}

The resolve method accepts two or three parameters. If there are two parameters it acts like version wrote earlier in the article. However, if there are three arguments it gets the first one, parses it and fills the deps array. Here is the test case:

var doSomething = injector.resolve('router,,service', function(a, b, c) {
    expect(a().name).to.be('Router');
    expect(b).to.be('Other');
    expect(c().name).to.be('Service');
});
doSomething("Other");

You will probably notice that there are two commas one after each other – that’s not a typo. The empty value actually represents the "Other" parameter. That’s how we will be able to control the order of the parameters.

Injection Directly Into the Scope

Sometimes I’m using a third variant of injection. It involves a manipulation of the function’s scope (or in other words, the this object). So, it is not always appropriate.

var injector = {
    dependencies: {},
    register: function(key, value) {
        this.dependencies[key] = value;
    },
    resolve: function(deps, func, scope) {
        var args = [];
        scope = scope || {};
        for(var i=0; i<deps.length, d=deps[i]; i++) {
            if(this.dependencies[d]) {
                scope[d] = this.dependencies[d];
            } else {
                throw new Error('Can't resolve ' + d);
            }
        }
        return function() {
            func.apply(scope || {}, Array.prototype.slice.call(arguments, 0));
        }        
    }
}

All we do is to attach the dependencies to the scope. The benefit here is that the developer does not write the dependencies as parameters; they are just part of the function’s scope.

var doSomething = injector.resolve(['service', 'router'], function(other) {
    expect(this.service().name).to.be('Service');
    expect(this.router().name).to.be('Router');
    expect(other).to.be('Other');
});
doSomething("Other");

Final Words

The dependency injection is one of those things which most of us do, but never really think about. Even if you didn’t know the term, you’ve probably used it million of times before in your code. Hopefully this article has given you a better understanding of how it works.

All the examples mentioned in this article can be found here.

This article was originally published at https://krasimirtsonev.com/blog/article/Dependency-injection-in-JavaScript

Previous

Six Reasons Why I Love Sass

LESS vs Sass? It’s time to switch to Sass

Next

Comments are closed.