神刀安全网

The current state of dependency inversion in JavaScript

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 ofInversifyJS (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.

The current state of dependency inversion in JavaScript

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 current state of dependency inversion in JavaScript

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 just that 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:

The current state of dependency inversion in JavaScript

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 dependency via its constructor he can just replace the real implementation at run-time. I don’t think that that 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. Would you still replace the class dependencies at run-time? 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.

The current state of dependency inversion in JavaScript

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.

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:

The current state of dependency inversion in JavaScript

The current state of dependency inversion in JavaScript

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:

The current state of dependency inversion in JavaScript

And the following for TypeScript:

The current state of dependency inversion in JavaScript

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:

The current state of dependency inversion in JavaScript

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.

The current state of dependency inversion in JavaScript

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:

The current state of dependency inversion in JavaScript

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:

The current state of dependency inversion in JavaScript

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

The current state of dependency inversion in JavaScript

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 current state of dependency inversion in JavaScript

The present: The InversifyJS DI approach

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

The current state of dependency inversion in JavaScript

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.

The current state of dependency inversion in JavaScript

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

The article about Multi Providers in Angular 2 by Pascal Precht explains the following:

The current state of dependency inversion in JavaScript

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.

The current state of dependency inversion in JavaScript

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

The current state of dependency inversion in JavaScript

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.

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 to TypeScript and because I keep an eye on the TypeScript issues on GitHub.

The current state of dependency inversion in JavaScript

We can guess that metadata will be stored using the Metadata Reflection API . But we don’t have enough information at this point to know out 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.

Be careful with the Angular and Aurelia documentation. Their dependency injection examples don’t showcase how to usage of string abstractions (literals instead) of concretions (classes) to avoid coupling. 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 :

The current state of dependency inversion in JavaScript

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.

We also have other framework-agnostic IoC containers like InversifyJS which feature support for advanced IoC container features like contextual bindings or interception .

Big frameworks supported 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 JavaScript code that adheres to the SOLID principles.

Thanks for staying with me it’s been a long read!

Please feel free to share thoughts about this article with us via @OweR_ReLoaDeD , @InversifyJS and @WolkSoftwareLtd .

Don’t forget to subscribe if you don’t want to miss it out future articles!

3
Kudos
3
Kudos

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » The current state of dependency inversion in JavaScript

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
分享按钮