Diving Deep with Dependency Injection

Injection

One of the coolest features of AngularJS is an often-misunderstood and somewhat widely undervalued component of the framework. It is a crucial component of the framework that allows the rest of the framework to work seamlessly and efficiently. Dependency injection is what gives Angular its superpowers.

Dependency injection in Angular is what allows us to write our Angular code without worrying about the order in which our code is loaded. It allows us to use the same syntax across Angular components and not concern ourselves with how dependencies arrive. It gives us the ability to write tests efficiently and not worry about how to separate production components from those in testing.

What is Dependency Injection?

Dependency injection is nothing new. It’s a software design pattern that allows us to remove hard-coded dependencies and allows us to change defined dependencies at either compile time or run time.

For almost every line of code we’ll ever write ever in any language, it’s likely that we’ll need to depend upon someone else’s code. These libraries are called dependencies (because we depend upon them for our code to run properly).

For instance, we would not write a printf method when writing in c. It is already defined for us in the libc library, thus we have created a dependency on the libc library.

At run time, our code needs to know how to find the dependencies on which we’re basing our functionality. There are only so many ways that our code can get a hold of these dependencies. Either:

The first approach requires us to maintain a global lookup table for our objects to look up dependencies. That is, when our code is running, it is the responsibility of our object to look up how to get its dependencies.

When using a library like jQuery, our browser holds onto the global jQuery object that is often referred to by the $ symbol. Although it doesn’t seem like we are looking for the dependency, the browser is handling looking up the object for us.

For instance, a service locator in JavaScript might look something like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var ServiceLocator = function() { this.services = {}; this.singletons = {}; this.registerService = function(name, serviceConstructor) { this.services[name] = serviceConstructor; }; this.getService = function(name) { if (this.services[name]) { if (!this.singletons[name]) this.singletons[name] = this.services[name](); return this.singletons[name]; } }; };

In order to use this ServiceLocator, our service requires knowledge of it. We can use this ServiceLocator through the global scope (just like how we’d look up jQuery) and our code would be happy, right?

1
2
3
4
5
// Theoretically, this might look like: app.service(function() { var sl = new ServiceLocator(); var http = sl.getService('$http'); });

This method creates a lot of complexity, especially when we are testing our app. Not only that, but it creates a tight coupling between the service and the ServiceLocator and it relies on the global scope.

We can use dependency injection, on the other hand, to dynamically link resource dependencies. Rather than requiring our objects to know how to fetch their own dependencies, we can simply hand them to our objects at instantiation.

In Angular, dependency injection looks like:

1
2
3
4
5
6
angular.module('myApp', []) .service(function($http) { this.getName = function() { // Use $http in here } });

When our service is instantiated, Angular will automatically pass the $http object so we can use it in our service. As developers of the Angular app, we only need to understand how to write our application such that the built-in Angular $injector knows how to fetch the dependencies (we’ll look at the $injector shortly).

For a deeper understanding of dependency injection, check out Martin Fowler’s post (the canonical resource on dependency injection), Inversion of Control.

Using dependency injection gives us a lot of flexibility at run time. Depending upon the context, we can inject the proper object into our Angular instances.

For instance, the $http service is built into Angular for us. The $http service object is an abstraction atop the browser’s built-in XMLRequestObject that works through another Angular service: $httpBackend.

When we’re running our app in the browser, Angular will inject the abstraction that makes the actual XMLRequestObject in the browser. When we’re running our app during testing, on the other hand, Angular will inject a mocked $httpBackend object that does not make the actual request. We don’t need to touch the production code; it simply works.

A side benefit of dependency injection is that Angular will inject the proper object depending upon when it is created. For instance, when the $injector instantiates a controller, it passes in a unique $scope object for the child controller. This is not possible in the first method.

This process only works because of dependency injection.

Angular’s $injector

Under the hood, this dependency injection is made possible through a built-in Angular service called the $injector. The $injector is responsible for instantiating objects, types, and methods and loading modules.

When creating an object, it:

When created, the following service object will have the services $http, $q, and the $rootScope available automatically:

1
2
3
4
5
6
angular.module('myApp', []) .service('GithubService', function($http, $q, $rootScope) { // The $injector found the $http, $q, and $rootScope // objects before the GithubService is instantiated // and it passes them in for us automatically. });

How to Specify Annotations

Okay, but how does Angular know what to inject into our Angular objects?

The $injector needs to be told what to inject. Traditionally, we provide this instruction through annotations where we tell the compiler what we want to inject. At compile time, the injector will look up which instances should be passed to the object and how to get them at run time.

In EcmaScript 5/JavaScript, however, we don’t have a way to specify annotations in our functions. The Angular team fakes annotations in a particularly novel way.

In JavaScript, we can call toString() on a method, and we’ll get a string representation of the actual arguments. With this string, we can split up the arguments (by their names) and use the names as annotations for the constructor function.

For instance, taking the constructor function from our above GithubService, determining the annotations might use a process like this one:

1
2
3
4
5
6
7
8
9
10
11
12
// angular.module('myApp', []) // .service('GithubService', function($http, $q, $rootScope) {}); var f = function($http, $q, $rootScope) {} var annotations = []; f.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1] .split(',').forEach(function(match) { annotations.push(match.replace(/\s+/, '')); }); // annotations now hold the annotation arguments // for the constructor function of the GithubService // ["$http", "$q", "$rootScope"]

Notice that since Angular is simply turning our constructor function into a string, order doesn’t matter when we specify the arguments of our function.

The two syntaxes are functionally equivalent:

1
2
3
angular.module('myApp', []) .service('GithubService', function($http, $q, $rootScope) {}) .service('GithubService', function($rootScope, $http, $q) {});

Minification

The astute reader might point out that if Angular uses the stringified version of the function to determine the right annotation, we need to address what happens when we minify our code.

Minification

Minifiers try to strip out as much of the source as possible to send the smallest file we can possibly send over the network. That means that minifiers will strip out comments and white space. They will turn long variable names into as few characters as possible. The above GithubService minified constructor function might look something like:

1
angular.module("myApp",[]).service("GithubService",function(e,t,n){});

When the $injector tries to determine what needs to be injected, it will find e, t, and n. The $injector will barf because it won’t be able to find the e, t, or n services.

There are other ways of specifying annotations on a particular service object. Rather than simply specifying the constructor function, Angular allows us to set the annotation as an array, of which the constructor function is the last element. As minifiers won’t shorten strings, we can explicitly set the dependencies as strings.

Again, looking at our above GithubService, we can redefine it to be minifier-safe like so:

1
2
3
angular.module('myApp', []) .service('GithubService', ['$http', '$q', '$rootScope', function($http, $q, $rootScope) { }]);

When we minify this version of the GithubService, we have the functional equivalent:

1
angular.module("myApp",[]).service("GithubService",["$http","$q","$rootScope",function(e,t,n){}])

It’s important to notice that with this version of the definition, order does matter. Angular won’t attempt to determine the annotation, as it assumes that we’ve specified it in the array. The order of the arguments will determine how Angular injects its dependencies.

The Future

Minification

The Angular team is working hard on getting Angular 2.0 out the door; they are writing Angular 2.0 atop the EcmaScript 6 (ES6) Harmony API. Since ES6 is not widely available yet (besides in nightly builds of the latest browsers and not without using a transpiler), the Angular 2.0 version of dependency injection described below is not available yet.

Note the JavaScript 2.0/ES6 specification is ever-changing, so this syntax may change by the time ES6 is finalized and released.

Since Angular 2.0 is based upon ES6, we get access to real classes.

For instance, using an incredibly simple example, let’s say we have three classes: Electricity, Switch, and LightBulb. The Switch is dependent upon the LightBulb class and the Electricity class. When we want to turn the light on, we’ll need to ensure the Electricity is turned on. Our classes might look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ES6 class Electricity { constructor(state) { this.on = state; } isOn() { return this.on; } } class LightBulb { constructor() {} turnOn() {} turnOff() {} } class Switch { constructor(electricity, lightbulb) { this.electricity = electricity; this.lightbulb = lightbulb; } flip() { if (this.electricity.isOn()) { this.lightbulb.turnOn(); } } }

Built-in to ES6, we can directly import the modules we need from the classes we’re specifying. This feature is important, as we no longer fetch our objects through a common, globally shared space. Instead, we explicitly import the files upon which we depend.

To import a component in ES6, we’ll use the import function:

1
import {Electricity} from './electricity';

Now, the import function doesn’t replace dependency injection; it only gives us access to the class-level method of the object we’re interested in. What we really want are real are annotations that describe the instance-level dependencies.

Although the following specification isn’t implemented in ES6, the community is considering the following annotation format.

1
2
3
4
5
6
7
8
9
10
11
import {Electricity} from './electricity'; import {LightBulb} from './lightbulb'; @Inject(Electricity, LightBulb) export class Switch { constructor(electricity, lightbulb) { this.electricity = electricity; this.lightbulb = lightbulb; } // ... }

Now when we boot our application, we won’t need to tightly bind functionality together; rather, we’ll use the injector as we normally would:

1
2
3
4
5
6
7
8
9
10
import {Injector} from 'di/injector'; import {Switch} from './switch'; function main() { var injector = new Injector(); // Load all the dependencies var switch = injector.get([Switch]); switch.flip(); }

Note that in our main() method we didn’t need to specify any dependencies other than what we are going to use. In this case, we’re using only the Switch. In the case of an Angular app, this would be the main module application.

Angular handles this main() method for us automatically, so we won’t need to mess with it at all. Angular will handle bootstrapping the app for us using this method, and the injector will take care of loading the required classes, just like in the previous versions of Angular.

This version of dependency injection is much cleaner and doesn’t rely on browser hacks to get it to work. No more dealing with strings, conflicts, global variables, weird module API methods, and minification hacks. It still provides the same benefits of dependency injection.

Lastly, it also allows us to use any ES6 module as a dependency. Have a favorite open-source ES6 module component? Just load it like any other Angular components.

Well, that’s a wrap for dependency injection. We hope you enjoyed today’s article. For more great Angular content, check out ng-book and don’t forget to sign up for the newsletter below.

Get the weekly email all focused on AngularJS. Sign up below to receive the weekly email and exclusive content.
We will never send you spam and it's a cinch to unsubscribe.

Download a free sample of the ng-book: The Complete Book on AngularJS

ng-book: The Complete Book on AngularJS is the canonical AngularJS book available today.

It's free, so just enter your email address and the PDF will be sent directly to your inbox. Mailchimp can take up to an hour to deliver the free sample chapter, but if you don't receive it within the hour, send us an email and we'll manually send them to you!

We'll send you updates about the book, when it updates and other free content.

We will never send you spam and it's a cinch to unsubscribe.

Comments

comments powered by Disqus