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 IoC 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 Car class inside the Engine 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:

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.