There are several ways to build a stable and testable application. Probably the most common approach is dependency injection (DI). The core DI concept is that components don’t look up their dependencies somewhere in a global context or using a service locator but simply declare what they need and their creator is the one responsible for delivering the dependencies.
In a dependency injection-based system, there are three required parts:
- a dependent component,
- a declaration of the component’s dependencies and
- an injector component that provides the dependent component with its dependencies.
One of the main reasons or advantages of DI is that components built in a way that adhere to the concept are easily testable. Because the component you are testing relies on its creator for providing it with its dependencies, we can actually pass a mock component to it and test it that way.
Now, let’s move to the code. We will have a user page controller and a user repository—the controller being the dependent component and the repository being the controller’s dependency.
function UserPageController(user_repo) {
this.user_repo = user_repo;
}
function UserRepository() { … }
It’s as simple as it can get. The controller simply expects a user repository component being passed to it’s constructor.
We can now have an injector component which will instantiate the controller. The issue is now how to tell the injector what are the actual dependencies. The most minimal way is simply hardcoding the declaration in the injector like this:
function createUserPageController() {
var user_repo = new UserRepository();
return new UserPageController(user_repo);
}
This is not very flexible as it requires us to modify the injector when the dependencies change. There are two possible ways of doing the declaration.
The first way is to parse the actual argument names of the constructor (from UserPageController.toString). This is fine in many cases but if you know me, you are aware of my need to have all the code compilable via the Google Closure Compiler. The problem you run in is that arguments are renamed in the compilation thus the injector gets a wrong declaration of dependencies.
What we need is an array listing all the dependencies stored on the constructor (or more precisely—for the compiler not to classify it as dead code—on its prototype).
UserPageController.prototype.$deps = [ 'user_repo' ];
The injector has to know to which component should it match each of the keys in the declaration. The odds are that most of the dependencies are going to be services (singleton-lifetime components). We are going to assume there are no “request-based” dependencies needed in constructors (we can inject them later via setter injection).
Your injector will probably be defined somehow similar to this:
function Injector() {
this.factories = {};
this.services = {};
}
Injector.prototype.addService = function (key, factory) {
this.factories[key] = factory;
};
Injector.prototype.getService = function (key) {
var service = this.services[key];
if (!service) {
var factory = this.factories[key];
service = factory();
this.services[key] = service;
}
return service;
};
Injector.prototype.create = function (Constructor) {
var Dependant = function () {};
Dependant.prototype = Contructor.prototype;
var instance = new Dependant();
this.inject(Constructor, instance);
return instance;
};
Injector.prototype.inject = function (Constructor, instance) {
var keys = Constructor.prototype.$deps || [];
var deps = keys.map(this.getService, this);
Constructor.apply(instance, deps);
};
Somewhere in the initialization code of your app, we need to instantiate the Injector and provide it with the service component factories.
var injector = new Injector();
injector.addService('user_repo', function () {
return new UserRepository();
});
Now we can simply ask the injector to create a controller instance and the injector will take care of the depencency injection:
var controller = injector.create(UserPageController);
Subclassing
A whole new problem arises when we want to subclass (inherit from) a constructor/prototype without breaking the DI.
The usual thing you do when extending is that you call the super constructor in the context of the new (extended) instance. There is a strong possibility that the new constructor will have different dependencies than the original one. This would mean that the new constructor would have to declare the original constructor’s dependencies as its own which does not really make much sense (at least in my opinion).
var Original = function (dep1) {
this.dep1 = dep1;
};
var Extended = function (dep1) {
Original.call(this, dep1);
};
The Extended constructor does not use the `dep1` service nor does it really need to know about it. The Original constructor should be called through the injector which would provide it with its dependencies.
The only issue here is how to provide the new constructor with the injector. The best way I came up with is to simply inject it in the constructor along with its other dependencies.
var Extended = function (injector) {
injector.inject(Original, this);
};
Extended.prototype.$deps = [ '$injector' ];
The Injector would feature itself under the key `$injector`:
function Injector() {
this.factories = {};
this.services = {
'$injector': this
};
}
I uploaded the complete Injector as a Gist. Feel free to clone it, fork it, use it and let me know what you think. Thanks!
by Jan Kuča