Learn about the past, present and future of dependency inversion in JavaScript

Over the last year and a half, I’ve been reading a lot about dependency inversion and taking a look to the source code of many open-source IoC containers for JavaScript. At the same time I’ve been working on the development of InversifyJS (a powerful IoC container for JavaScript apps powered by TypeScript).

I have spent a lot of time thinking and talking about DI in JavaScript with many developers and I found this topic to be quite controversial. I’m writing this post to share what I’ve learn.

Debunking the JavaScript IoC container myths

Before I go into details about past, present and future of dependency inversion in JavaScript I will try to debunk what I believe are some myths.

Myth 1: There is no place for IoC containers in JavaScript

When I have a conversation about decoupling and modularity in JavaScript applications, many developers defend that IoC containers have no place in JavaScript because it is an extremely dynamic programming language.

JavaScript is a multi-paradigm programming language, it has some elements from functional programming and some elements from object oriented programming but the nice thing about JavaScript is that it is so dynamic that you can extend it to meet the style that you like the most.

The Object-orientation elements of JavaScript in ES3 and ES5 were not as easy to spot as they are in ES6. At the same time, functional programming is becoming a main stream topic and we are going to see much more pure functional programming frameworks and libraries like ramdajs in the future.

Some people will follow the functional path and maybe IoC containers will not be a requirement.

However, other people will follow the object-oriented path and IoC containers will become an absolute necessity as a result of the growing complexity of JavaScript applications.

Note: The term complexity is used in this context to describe the interactions between a number of entities. If the number of entities and interactions between them increase, we will get to a point in which it would be impossible to know and understand all of them.

The majority of us would never say something like “I don’t need to follow the solid principles” when coding in C# or Java. Why would you say the same when you choose to use the OO programming paradigm in JavaScript?

We also have a third kind of frameworks and libraries that will take ideas from functional programming and OO programming and mix them. In this kind of architectures is not clear if an IoC container will be necessary I guess it will depend on how much it is aligned towards the functional style or the OO style…

Myth 2: We don’t need IoC containers, we already have module loaders!

Another common misconception is that we don’t need IoC containers in JavaScript because we have module loaders.

We are all really happy about modules. Our JavaScript code was an absolute nightmare before they arrived and, now that they are available, we are able to enjoy certain level of decoupling.

If you have a not very large project, modules could be enough to manage the complexity (number of entities and interactions) of your application but modules is not enough to adhere to the dependency inversion principle:

When you import a module you are importing a concretion, the module that you are about to consume, you are not importing an abstraction of that module. Consider the following code snippet:

The DataService class has a dependency on the Http class. We have used modules but we have a hard coded reference to the Http class. If the name or path of the Http class changed, we would have to update all the modules that depend on it. This means that our code is tightly coupled and it is not maintainable, testable or re-usable.

In the old days we declare all these entities in the same file or use multiple files and <script /> tags to load the files in the correct order. We had to avoid polluting the global scope and we started to use namespaces and closures to solve this problem. Modules help us to solve these problems but we shouldn’t think that just because two entities are defined in two different files they are not tightly coupled.

Myth 3: Dependency inversion === injecting dependencies

We have already mentioned that we shouldn’t think that just because two entities are defined in two different files they are not tightly coupled. In a similar manner we shouldn’t think that just because we are injecting dependencies the dependency and the dependent are not tightly coupled.

Let’s consider the following code snippet from an Angular 2.0 example:

This time we are injecting the dependencies of the DataService via its constructor. This will facilitate things like writing unit test because we will be able to inject a stub of the Http class into the DataService with ease.

However, we are still importing the module in which the Http class is declared. We have implemented dependency injection but we have not achieved dependency inversion. If the Http class changed its name or location, we would have to update all the modules that depend on it.

David Heinemeier Hansson (author of ruby on Rails) argues that he doesn’t need dependency injection because Ruby is a dynamic programming language. He explains that when he is unit testing a class, instead injecting stubs of the class dependencies via its constructor, he can just replace the real implementations at run-time. I don’t think that this is a good practice but let’s say that it is OK for testing. It would still be definitely wrong for maintainability and re-usability reasons. Imagine that you import that class in another application or in another module. You would need to replace the class dependencies at run-time and your code would become really hard to maintain and read.

Note: The above is a TypeScript example. TypeScript is able to know the server to be injected because we have indicated its type. If we used ES5 instead we would have to indicate the type to be injected manually using something like the following:

DataService.parameters = [new ng.core.Inject(Http)];

This means that even when using ES5 we would still need to include a reference to the Http module.

Injecting a dependency is not the most important thing. What truly matters is depending upon an abstraction.

Only by doing this we can remove the hard coded reference to the Http module.In this case, if the Http module changes its name or location, all the modules that depend on it would remain the same because they are not aware of it.

Depending upon an abstraction allow us to achieve real decoupling but the beautiful thing is that the decoupling goes beyond a dependency and a dependent. We can also decouple some cross-cutting concerns like caching or logging. using interception.

Interception is an advanced programming technique that allows you to intercept calls to an object so that you can add additional logic before or after the call. This interception process should be virtually transparent, so both the calling object and the target object can be oblivious to the interception process.

In summary, dependency inversion is much more than injecting dependencies.

The journey to SOLID JavaScript

Now that we know that dependency inversion in JavaScript is possible and necessary we are going to examine how far are we from being able to write JavaScript applications with real decoupling.

SOLID (single responsibility, open-closed, Liskov substitution, interface segregation and dependency inversion) is a mnemonic acronym that stands for five basic principles of object-oriented programming and design that intend to make it more likely that a programmer will create a system that is easy to maintain and extend over time.

The early days: module loaders

As we have already exposed module loaders were the first step towards real decoupling in JavaScript but they are no more than that: just the first step.

The recent past: The angular 1.x DI approach

We can say that Angular 1.x was the first JavaScript framework that introduced dependency injection and its IoC container (the $injector) as one of its value propositions.

The Angular 1.x allows us to declare the dependencies of a module in two different ways:

In both ways we are using string literals to refer to the dependencies to be injected. We can consider the string literals as abstractions of the dependencies. This means that Angular 1.x allow us to depend upon abstractions and declare modules with real decoupling.

Unfortunately not everything is good news and as Pascal Precht described at http://blog.thoughtram.io the Angular 1.x DI implementation has some problems:

Internal cache - Dependencies are served as singletons. Whenever we ask for a service, it is created only once per application life-cycle. Creating factory machinery is quite hairy.

Namespace collision - There can only be one token of a “type” in an application. If we have a car service, and there’s a third-party extension that also introduces a service with the same name, we have a problem.

Built into the framework - Angular 1’s DI is baked right into the framework. There’s no way for us to use it decoupled as a standalone system.

The present: The Aurelia DI approach

One of the modern frameworks that includes dependency injection as a feature is Aurelia. Aurelia proposes the following for JavaScript:

And the following for TypeScript:

As we have already learned we shouldn’t think that just because we are injecting dependencies the dependency and the dependent are not tightly coupled. In both of the code samples above there are hard coded references to the HttpClient:

import {HttpClient} from "aurelia-fetch-client";

However, it looks like we can use string literals to solve this problem:

After depending upon an abstraction, we need to link the abstraction (string literal/interface) to an actual concretion (class). We can do this invoking the registerTransient method of the Aurelia IoC container during the bootstrapping process.

This means that the IoC container is aware of the HttpClient class but the User class is only aware of its abstraction. Another good thing is that all the coupling in an Aurelia application can be contained in one unique place within the entire application: the bootstrapping process.

The bootstrapping process allow us to configure the IoC container. The IoC container is aware of all the entities in the system but the entities are not aware of each other.

Note: Avoid thinking about the bootstrapping process (IoC configuration) as the Composition Root they are not the same thing.

The present: The Angular 2.x DI approach

The Angular 2.x IoC container was created to solve the problems of the Angular 1.x IoC container.

The majority of examples that I have found online look like the following:

Once more, we have a class DataService declared in a file which contains a hard coded reference to its dependencies: The Http class.

Just like in Aurelia, we can bind a string literals to a class solve this problem:

We can inject the DataService class into a component using the providers setting:

This is cool but it is nicer to centralize all the coupling in one unique file within the entire application. This can be achieved in an Angular 2.x thanks to the bootstrapping process:

The present: The InversifyJS approach

InversifyJS has been designed to encourage the adherence to the SOLID principles and allows you to depend on abstractions (interfaces):

Abstractions (interfaces) are not available at runtime so we use string literals instead. Just like Angular and Aurelia, we can expect string literals being generated by the TypeScript compiler in the future.

As we can see above InversifyJS tries to take advantage of the TypeScript type safety by the usage of features like generic types. InversifyJS is framework-agnostic and supports advanced IoC container features like contextual bindings or interception.

All the coupling in an InversifyJS application can be contained in one unique place within the entire application: the kernel configuration.

InversifyJS vs Angular 2.0 DI

When I first saw the title of the post, I thought that it was going to be like what I call “multi-injections” in InversifyJS but I was wrong.

We can say that multi-providers are the way Angular 2.x handles what I call “contextual bindings” in InversifyJS.

I personally like more the InversifyJS approach because it ensures that all the bindings (the mappings between abstractions and concretions) are declared in one unique place. The InversifyJS docs encourage the creation of a file named inversify.config.ts. This file contains the IoC container configuration: all the binding declarations and constraints (contextual bindings). The IoC container is aware of all the entities but they are not aware of each other. This allow us to centralize all the coupling in the entire application in one unique place: the inversify.config.ts file.

If we have a huge application we could create multiple IoC container modules to make the inversify.config.ts file easier to maintain.

The future

The future is about design-time type annotations available at run-time as metadata. I know this because I’ve seen how developers have been asking for it all around the internet ever since decorators were added to TypeScript and because I keep an eye on the TypeScript issues on GitHub.

We can guess that the metadata will be stored using the Metadata Reflection API. But we don’t have enough information at this point to know how the type annotations will be represented at run-time. However, we can guess that it will be one of the following:

String literals

Symbols

Object literals (JSON)

This means that we can expect that in the future string literals will no longer be necessary to achieve real decoupling with Angular 2, Aurelia or InversifyJS. The metadata will be generated by the compiler so we can also expect to see more and more JavaScript transpilers.

Summary

Be careful with the Angular and Aurelia documentation. Their dependency injection examples don’t showcase how to use abstractions (literals literals) instead of concretions (classes) to avoid tightly coupled dependencies. I assume that they don’t show it because string literals are ugly but they are the only current way to achieve real decoupling.

We need string literals because TypeScript interfaces are not available at runtime. This is caused by the way the TypeScript compiler handles the generation of design-time type metadata and we can expect it to be fixed in the near future.

Meanwhile, please avoid using magic strings all around your code. Try to declare all your string literals in a constants file like action in Redux:

We are about to implement real JavaScript IoC containers without compromises (string literals) and two of the most promising JavaScript frameworks Angular 2.0 and Aurelia include an IoC container and encourage the implementation of the SOLID principles.

Big frameworks supported by big companies like Google and Microsoft are using IoC containers in JavaScript. It is time to stop thinking that IoC containers have no place in JavaScript applications and to start writing OO JavaScript code that adheres to the SOLID principles.