Dependency Injection and Inversion of Control in JavaScript

Viktor Kukurba
10 min readSep 8, 2019

--

The active growth of web technologies leads to the resolving of complex solutions on both the server and the frontend side. In case of developing the server part, there are many alternative solutions to use, other than JavaScript (JS) / NodeJs. Then in case of frontend, there are no alternatives to JS. This causes a significant development of the JS programming language and the release of the new ECMAScript specifications, which push to the effective use of JS in the world of programming.

This article is written to show the important principle of programming -Inversion of Control (IoC). We will consider its implementation using the Dependency Injection design template. In the context of this topic, we will define the Dependency Inversion Principle and the implementation of the IoC container. Also, we will look at the implementation options in the JavaScript and TypeScript (TS) example, prospects, features and weaknesses of the JS / TS in this topic. The main goal is to understand the idea and approaches related to the topic of the article.

It is difficult to overestimate the importance of abstraction in the development of complex systems.

Basic Definitions

For a step-by-step understanding of this topic, we will try to move from the definition of common concepts to their explanation and examples. To get an initial understanding of the context of this article’s subject we will need to bring light to the basic concepts and definitions.

Inversion of Control (IoC) is an abstract programming principle based on the flow of control (execution of statements/instructions) that should be fully managed by the specific implementation of the framework, which is external to your code.
Let’s consider the following comparison: in the traditional approach, your code executes methods from the library/framework, and the inversion of this approach means that your code is called by the library/framework. Of course, it is better to say a framework - rather than a library. You register parts of your code (methods, classes, modules, etc. …) in the IoC framework that resolves itself when it has to call your code.
This comparison is rather rough but good enough for the initial understanding and it gives direction for the further and deeper perception of this concept.

A Dependency is an object or any software programming unit that is used by the client program or software unit. For example, the dependency is a service that returns data that must be represented in the client (web component), and so on. Those kinds of objects are often used in different places of the project.

Dependency Injection (DI) is one of the implementations of IoC based on the composition of dependencies in a client (dependent unit).

Dependency Inversion Principle (DIP) is one of SOLID’s principles, which satisfies the following:
- the upper levels modules do not depend on implementations of the lower levels ones. Modules should depend on abstractions;
- abstractions do not depend on the details, but the details depend on abstractions;

The IoC-container is the implementation of the described principles in the form of a framework, library or module that facilitates the writing of a code and takes care of dependency injection and class instantiation.

Let’s try to look into more details of these concepts including examples in JavaScript.

Inversion of Control

This principle takes place in every modern UI framework. As an example, let’s consider the classical approach to implementing a router (Vue Router, Angular Router, React Router, etc.). We should specify a map (preferably an array of maps) where the key is an URL path, and the value is a Javascript class. In general, we actually set the configuration for the router, and the framework itself listens to the hashchange event and creates instances of the corresponding classes that render the desired page.
Similarly, with the component approach in the above-mentioned frameworks (Vue, Angular, React), the creation of Javascript classes (that describes the components), is concerned with the framework, while the LifeCycle hooks on the component are invoked by the framework.

The considered frameworks specify a skeleton where the client code is written, the execution of which is related to the framework.

The above examples follow the IoC principle. In the context of our topic, it is important to emphasize that we have the ability to configure and implement certain classes or functions that will be called not by our code, but by the frameworks.

Dependency Injection

Let’s consider one of the options for implementing the IoC principle in details. We have two classes of Car and Engine, and the first one has a dependency on the other.

Our task is to get rid of the dependency of the Engine class inside the Car class. By dependency we mean the creation of an Engine instance inside the Car constructor. The Car property this.engine still must be an instance of Engine.

So, in the next step, we will consider three options for solving this problem using DI:

  1. Constructor injection. In this case, the dependency is passed as a parameter in the constructor method.

Here, the context in which the client is created is responsible for the creation of the dependency instance. The advantage of this approach is that after creating an instance, all of its composite parts (dependencies) are defined to the client. However, there is no opportunity to change the value of dependency without creating special methods, and it is necessary to have references to instances of dependencies before the creation of a client.

2. Setter Injection requires to implement a setter that transfers dependency on the client that is used:

In this case, the situation is possible when the client instance is already in use and the dependency is not set yet; on the other hand, it is possible to create and use the client without dependency or to replace the dependency in the process of executing the code.

3. An Interface Injection requires the implementation of an interface which is a set of methods or setters that stores dependencies within the client. The outer scope or special injector uses this interface to communicate with the client by setting or replacing dependencies.
As JS does not support interfaces we will consider the TypeScript example. In case of pure JavaScript, this interface can be maintained at the level of the conventions between the engineers, or by the creation of the base class.

This option works well for the family of classes that may have the same dependency, which in certain situations needs to be updated in all the clients.

Dependency inversion principle

The DIP has a broader meaning than DI that offers ways to solve the problem, while DIP formulates the principles of creating independent software modules(units, classes so on…) that interact with each other.
In the previous paragraph we managed to “separate” the Engine class from Car with DI. At first glance, it seems that we have met the requirements of the DIP principle because we can keep the Engine and Car classes in separate files, or even modules. However, if we look at the last two implementations, we can see that in the Car class there is a link to Engine, type verification (instanceof Engine and private engine: Engine). In fact, it is a dependency on a particular implementation, not an abstraction. In the TypeScript, as in many other object-oriented programming languages, this problem is solved using interfaces or abstract classes. Those are used for type definition purposes private engine type: IEngine So, now the types of properties of the Car class allow you to get rid of importing Engine class.

However, the current implementation of interfaces in TypeScript and of course in JavaScript is not so good as we would like to have it. Let’s start with the fact that there are no interfaces in JS at this time, and in the latest versions of TypeScript (3.4) they are not available in runtime yet. This is the reason why IoC containers in JS / TS instead of interfaces often are used string literal or Symbol as keys (token). In the next parts of the article, by the interface or token we will mean string literals or symbols that correspond to specific implementations registered in the IoC containers.

IoC container

As we could see in case of using DI approach, there is a need to create dependencies outside of the classes that use them. Unlike trivial examples in more complex projects, there is a need to do this centrally. That is the main purpose of IoC containers, which maps “interfaces” and their specific implementations.

One of the first implementations of DI with the IoC container in JavaScript appeared in Angular 1.x. Other popular frameworks (Aurelia, InversifyJS, Angular 2+) that implement IoC containers in JavaScript use TypeScript. This brings a good level of abstraction that is important for building good implementations of the IoC frameworks.
This paragraph could be divided into several articles to cover the details of the implementation and to bring light to the magic that takes place under the hood of the IoC container approach. However, the purpose of this article is the usage of IoC containers, not their implementation. Therefore, the main points will be rather superficial to indicate the way it works. At the first stage of dealing with this approach, this is quite enough for the effective use of such frameworks.

So, an interface, abstract class, and strict typing are used in the implementations of the IoC container. That helps to separate the abstraction and implementation layer. Although based on Angular 1.x, we can make sure that this is not critical, but for complex, projects with the large codebase, it greatly affects code quality and development performance.

Also, classical IoC implementations use decorators that provide a convenient interface for marking specific classes (‘@ injectable’) that can be used as dependencies. In client class, the dependencies are labeled (‘@ inject’)that are tokens and are created using the IoC container. Dependency specific implementation will be known at the time of execution (runtime) and will be passed to the instances of client classes that require its injection.

The decorator methods use the polyfill “reflection-metadata” that implements the Metadata Reflection API. That covers one of the goals to save and retrieve metadata information about types. Methods Reflect.defineMetadata and Reflect.getOwnMetadata are used for that purpose. That makes possible getting information about dependencies during creating the instances and up to injecting (usually send to the constructor method).

To be able to use decorators and metadata in TS, the following options should be specified in the configuration file (tsconfig.json):

"emitDecoratorMetadata": true,
"experimentalDecorators": true

Let’s consider the trivial example of using the IoC container with InversifyJS framework.

First, describe the abstraction layer.

We’ve defined the interfaces which our program will depend on. Also, there should be no dependency on the specific implementation of these interfaces.

In the next step, consider the trivial implementation of the described abstractions.

So, here we have three classes decorated with @injectable() that allow us to link them to the abstract layer in the next step.

It is common to implement the mapping (configuration) in the separate program file.

container.bind<IConfiguration>(TYPES.Configuration).to(SConfiguration);
container.bind<IVehicle>(TYPES.Vehicle).to(Car);

You can also implement an interface in this configuration file for changing implementations in run-time, depending on build, environment settings, or other external factors. An alternative option will be to connect different configuration files — abstraction mapping to one implementation or another one based on the described conditions.
Let’s illustrate it with an example of dynamic dependency change (a function is implemented in a configuration file)

export function useXConfig() {
container.unbind(TYPES.Configuration);
container.bind<IConfiguration>(TYPES.Configuration).to(XConfiguration);
}

The main program code will not change.

export function main() {
const myCar = container.get<IVehicle>(TYPES.Vehicle);
console.log(`myCar: ${myCar.description}`);
}

In the current case, we will get

main(); // myCar: Model S white

But after executing the function useXConfig(); that can be executed by specific properties of the environment, behavior, user location, build settings, etc., we get

main(); // carX: Model X black

The trivial example described above is intended to show a program code consisting of abstraction, implementation, configuration, and main program, which has no dependencies on implementations. Usually, most changes occur in the main program and implementations. The abstraction and configuration ensure the stable and correct operation of the system, that is constantly updated, expanded, tested and executed in different environments.

Summary

Dependency Injection approach and IoC-container frameworks take place in JS. TS provides variability and ways for classical implementation in sense of the abstraction level and resolving instances using decorators. Despite TS options, there are still gaps in the Metadata Reflection API but those can be handled by corresponding polyfills. The problem with inaccessibility to TS interfaces at runtime is fixed by workarounds using Symbol and strings. Surely, the work on addressing these shortcomings will continue in the near future, and eventually, it will be fully implemented.

Let’s consider the strengths and weaknesses of the approach discussed in the article.

Advantages.

  • Provides simplicity and flexibility for the unit testing, which is achieved by the ability to create simple “plug-ins” for complex dependencies and test only the target functionality in specific tests.
  • Reduces the dependencies between software units. That has a positive impact on the prospects of expanding and scaling the system.
  • Gives additional options to provide configurability for the system.

Disadvantages.

  • Increases the complexity of the system structure by new files and separating program behavior from creating software components.
  • Code reading and System debugging/tracing are complicated because of the errors that could not be detected at compile time but only at runtime.
  • Reducing the ability to use IDE automation features for link search, typing, and more.
  • Higher threshold for entry into the project development and support for system building principles.

Usage of the described approach can have a positive and negative impact on software development. Regardless of the principles, paradigms, and patterns of programming, it often depends on how we can apply ideas in concrete solution, and how often system requirements can change. Therefore, the experience, understanding, and perceptions of the strengths and weaknesses of the development team are important. It is clear that this principle is better suited to large systems, enabling them to be broken down into simpler subsystems with defined dependencies. This is confirmed by the popular opinion that Angular 2+ (the role of DI in which is difficult to overestimate) is more suitable for large systems. But still, in my opinion, other popular frameworks are also quite competitive in solving complex problems.

In addition, too much attention has been paid to using TypeScript. From my point of view, knowledge of it is necessary for modern web development because it is actively used with all of the popular JS frameworks not only Angular 2+, including server-side NodeJS.

At the same time, we can make simple implementations too complicated, so we should rely on our own experience and team level. In any case, you should be open to new approaches, follow trends, and broaden your knowledge and skills, and, of course, not just programming ones.

--

--

No responses yet