Dependency Injection

Sooner or later, while learning programming, we run into design patterns. Once we get to know them, it turns out they are everywhere, and we use them even when we do not realize it. Today, let us look at one of those patterns - Dependency Injection.

Botox Injections

From this article you will learn:

  • How does the Dependency Inversion Principle work?
  • What is Inversion of Control?
  • What is Dependency Injection?
  • What is the Hollywood Principle?
  • What kinds of dependency injection do we have?
  • What are the pros and cons of Dependency Injection?

For quite some time I had been planning to start a series about design patterns. Design patterns are simply recognized, usually optimal solutions to common problems we meet while programming. Following that thought, a design pattern is not a specific piece of code we can paste in to solve a problem we have just found. It is rather a concept that we can adapt to the project we are working on.

Is This My Dependency?

The applications we build very often consist of many different elements. These fragments, for example classes, are responsible for separate parts of our business logic. Many times, however, we may come to the conclusion that one part of our application needs to use logic hidden in another part. When more and more such dependencies appear in the code, chaos starts to grow. Still, chaos caused by the number of connections is not the only threat to our code.

The last of the SOLID principles, the Dependency Inversion Principle, tells us that connections between our classes should be loose. This means that higher-level classes should not depend on lower-level classes, and abstractions should not depend on details. It should be the other way around. In other words, an implementation detail is something in our application that should live at the highest layer. Exactly like in the real world: humans belong to the kingdom of animals, the phylum of chordates, the subphylum of vertebrates, the group of jawed vertebrates, the class of mammals, the order of primates, the family of hominids, and finally the species Homo sapiens. Notice that the higher we go in this taxonomy, the more detailed the features of the described organisms become. For example, the unquestionable lack of a tail is a feature of hominids, but not of primates in general. In the animal kingdom, however, we will include multicellular, heterotrophic organisms whose cells have a nucleus. Basically, both a human and a tapeworm. It would be difficult, though, to say that a tapeworm belongs to our species. At a certain level of abstraction we may therefore assume that all life on earth is the same, but it is the details that create diversity.

Programming works in the same way. Abstraction helps us describe general assumptions, but implementation details will be somewhere higher and will make our classes serve different purposes. Moving too much detail into the abstraction layer may disturb the logic in our application and additionally complicate code whose maintenance and further development will become much more demanding.

If we were to write software for semi-autonomous cars, we would probably have quite a lot of functionality related to notifying the driver about road hazards. The hazards themselves would come from different parts of the application responsible for things like lane keeping or road sign analysis.

js
class Car {
	constructor() {
		this.notificationService = new NotificationService();
	}

	pushNotification(msg) {
		this.notificationService.push(msg)
	}
}

const car = new Car();
car.pushNotification('Warning! Crooked road sign!');

In the example above, the service responsible for notifications is permanently "baked into" the Car class. Such a structure means that the Car class must know everything about the dependencies of the notification service. It is easy to see that the level of dependency coupling generated this way may become very high.

Additionally, changes in the NotificationService class will force changes in the Car class. This looks like a perfect recipe for an application that will be fairly expensive and difficult to maintain.

If we developed our application without dependency management, we would reach a situation where the complexity of our code grows together with the number of connections.

Do Not Call Us. We Will Call You.

"Do not call us. We will call you" is the content of the Hollywood Principle. This principle says that individual elements of our system should not care where incoming requests come from or where they go next. Each module should focus mainly on its own task, meaning the execution of a given call. In other words, a class should wait until it receives a task, perform it, and that is it. It definitely does not need to know which module asked it to do something, nor how that work will be handled in the next stages.

From the Hollywood Principle we can move directly to a programming paradigm called Inversion of Control. In the classic approach, the programmer has full control over the executed code. In practice, this usually comes down to a situation where we have to rigidly define the order in which individual functions in our code are called, ensure consistency, and follow the rules of control flow. In an approach using Inversion of Control, our program will consist of many parts independent of any particular implementation. Let us compare the two examples below:

js
const taskName = prompt('Enter task name');
const transformedTaskName = transformTaskName(taskName);
const taskDescription = prompt('Enter task description');
const transformedTaskDescription = transformTaskDescription(taskDescription);

const generatedTaskDOM = prepareTaskDOM(transformedTaskName, transformedTaskDescription);

const app = document.getElementById('app');
app.appendChild(generatedTaskDOM);
js
function TaskModule () {}

TaskModule.prototype.transformTaskName = function() {}
TaskModule.prototype.transformTaskDescription = function() {}
TaskModule.prototype.prepareTaskDOM = function() {}
TaskModule.prototype.init = function (name, description) {
  const transformedName = this.transformTaskName(name);
  const transformedDesc = this.transformTaskDescription(description);

  const generatedTaskDOM = this.prepareTaskDOM(transformedName, transformedDesc);

  const app = document.getElementById('app');
  app.appendChild(generatedTaskDOM);
};

const taskModule = new TaskModule();
const taskName = prompt('Enter task name');
const taskDescription = prompt('Enter task description');

taskModule.init(taskName, taskDescription);

Notice that the main part of our application, in the second example, is not responsible for the order of transformations and operations on input data. Only TaskModule has that knowledge. In the first example, the programmer has to decide when each operation happens. In the second one, the TaskModule has to know that, and the rest no longer does. It may seem that in the second example we lose control over what happens in our code, but in reality we gain something else: we can focus on delivering the business value of our application.

Dependency Injection

Dependency Injection, the pattern from the title, is nothing more than a way to implement the principles described above. It is currently the most popular implementation of Inversion of Control. It is therefore a mistake to identify IoC, or Inversion of Control, with DI, or Dependency Injection. IoC is a general concept, while DI is its specific implementation.

This design pattern originally comes from the Java programming community. The general rule is that a class will not create new instances of other objects that are used inside it. As a result, we do not create detailed connections between objects, and our class does not have to pass any parameters to a newly created object. In the Java world, it is best to work with class interfaces, not with their concrete implementations.

There are several ways to use Dependency Injection. One of them is constructor injection.

Constructor Injection

What does it look like in practice?

js
class TaskModule {}

class App {
  constructor(taskModule) {
		this.taskModule = taskModule;
		this.init();
	}

	init() {
		this.taskModule.init();
	}
}

const taskModule = new TaskModule();
const app = new App(taskModule);

Notice that in the example above, a new instance of TaskModule was created before the instance of the App class and was then passed to that class constructor. In this approach, we still need to remember dependencies of dependencies and keep the correct order of calls.

In Java it would look a little different. Here we do not need to call the constructor of our TaskModule class, because we pass only its type to the App class. Working with types in JavaScript is unfortunately not that simple.

java
class TaskModule {}

class App {

   private TaskModule taskModule;

   public App(TaskModule taskModule) {
       this.taskModule = taskModule;
   }
 
   public void init() {
       this.taskModule.init();
   }
}

Setter Injection

Another way of injecting dependencies, besides the method shown above, is setter injection. It means that we do not pass the dependency through the constructor, but instead place the instance of the injected service directly into the instance of the class we want to extend.

js
class TaskModule {}
class App {}

const app = new App();
app.taskModule = new TaskModule();

Setter Injection was historically the first way of injecting dependencies promoted by the Spring Framework in Java. As early as 2003, Spring based its architecture on this method of dependency injection. At that time, the creators of Spring came to the conclusion that using constructor injection was less readable for programmers.

java
public class TaskModule {
	public TaskModule() {}

	public void init() {}
}

public class App {
	TaskModule _taskModule;

	public App() {
		_taskModule = new TaskModule();
	}

	public void init() {
		this._taskModule.init();
	}
}

Pros and Cons

Because Dependency Injection is an implementation of Inversion of Control, we can point to similar advantages of using it, such as:

  • forcing us to create code in small classes that have their dedicated goals;
  • removing dependencies between classes in favor of a plug-in architecture;
  • making testing easier by allowing us to replace an injected dependency with our own implementation of it, most often a mocked one.

However, it also has its disadvantages:

  • lower code readability when there are too many dependencies;
  • if a class is supposed to be serialized, it may be necessary to define a default constructor;
  • when complexity becomes too high, the class should be reorganized and split into smaller parts, so that the dependencies are actually needed in most of the methods that use them.

Summary

As we can see, Dependency Injection is not a very complicated design pattern. It is often recommended because of its high flexibility and easier testing. We can find it, for example, in Angular or Spring. I realize that the topic has not been exhausted yet, but I tried to describe it as broadly as possible in a compact form.

Sources

Share this article:

Comments (0)

    No one has posted anything yet, but that means.... you may be the first.

You may be interested in

If this article interested you, check out other materials related to it thematically. Below you will find articles and podcast episodes authored by me, as well as books I recommend that expand on this topic.

SSR, SSG, SPA czy MPA? by Mateusz Jabłoński
Podcast
31 January 2023

SSR, SSG, SPA czy MPA?

W pierwszym odcinku podcastu PiwnicaIT rozmawiamy o różnych podejściach do tworzenia aplikacji webowych. Poruszamy tematy związane z SPA, SSG, SSR czy MPA w ujęciu webdevelopmentu. Omawiamy nasze doświadczenia w pracy z różnymi bibliotekami i frameworkami dostępnymi na rynku.

Posłuchaj
Matryoshka Dolls by Frankenvrij
Article
2021-08-13

Currying

Functional programming is almost as popular as object-oriented programming. Many concepts from object-oriented programming have entered programming in general so deeply that sometimes we no longer even notice where a given approach comes from. Functional programming also has its interesting concepts, and currying is one of them. In this article I use examples to show how currying works and what problems it can help us solve.

Read more

Zapisz się do newslettera

Bądź na bieżąco z nowymi materiałami, ćwiczeniami i ciekawostkami ze świata IT. Dołącz do mnie.