secture & code

SOLID Principles (5): Dependency inversion principle

To finish with the 5 SOLID principles, today we are going to talk about the last of these, the “Dependency Inversion principle”.”

Dependency Inversion Principle

In the 1990s, Robert C. Martin (colloquially known as Uncle Bob) formulated the fifth principle of the 5 SOLID principles. The principle known as “Dependence Inversion”). 

dependency_investment_principle

What is the Dependency Inversion Principle?

The principle recommends: 

  • High-level modules should not depend on low-level modules. Both must depend on abstractions (interfaces).
  • Abstractions should not depend on details. Details should depend on abstractions.

How to detect that we are violating the principle of “Dependency Inversion”?

One of the easiest ways to detect whether we are violating the principle is to assess whether we are relying on low-level modules in a high-level module. 

Another sign that should attract our attention is if we notice that, in our high-level modules, we have no or a lack of interfaces.

What happens if we change a low-level module? If when answering this question we realize that in order to change a low-level module we have to make changes in the code of the high-level module, we are not using the principle correctly. 

Another important point for violation detection is the unit tests (unit tests). If when performing unit tests we realize that they have a great complexity due to dependencies, we can begin to suspect that we are violating the principle.

How to solve the violation of the principle?

The best way to solve the violation of the principle is to create interfaces and abstractions. With this, the high-level modules use implementations of the low-level classes. By using these implementations we stop relying on the low-level modules and therefore stop violating the fifth of the SOLID principles. 

Example:

Let's imagine that we have a user login service. This service has to check if the user exists in bbdd and send an email sending an access link to the account.

This service could be something similar to this:

class LoginUser {
  private repository;
  private mailer;

  builder() {
    this.repository = new UserMongoDBRepository();
    this.mailer = new GmailMailer();
  }

  login(email: string) {
    const user = this.repository.getUser(email);

    if (user) {
      this.mailer.sendLoginEmail(email);
    }
  }
}

Here we are directly using the Mongo repository and the Gmail library directly, i.e. the service is relying on low-level modules, not implementations.

This would make unit testing very complicated. In addition, if we wanted to change the low level module (for example, change Gmail for another email sending platform) we would have to modify our code.

To avoid this, we can create two interfaces, one for accessing the database and one for sending emails. These interfaces could be something like this:

interface UserRepository {
  find(email: string);
}

interface Mailer {
  send(email: string)
}

Once we have our two interfaces, what we have to create are the services that will implement them:

class UserMongoDBRepository implements UserRepository {
  find(email: string) {
    // Access to mongo to obtain the user (getUser method)
  }
}

class GmailMailer implements Mailer {
  send(email: string) {
    // Access to the GMAIL api to send the data (sendLoginEmail method)
  }
}

With these interface implementations we are creating an abstraction layer, because if we wanted to change the method of sending emails, we would have to do the following:

class XMailer implements Mailer {
  send(email: string) {
    // Access to X api to send data (e.g.: sendMailX method)
  }
}

The login service using the implementations would look as follows:

class LoginUser {
  builder(
    private readonly repository: UserRepository, 
    private readonly mailer: Mailer) {}

  login(email: string) {
    const user = this.repository.find(email);

    if (user) {
      this.mailer.send(email);
    }
  }
}

At the point where the service is required LoginUser, we can indicate which implementations we want to use. As all the services implement an interface, we get all the code to work in a correct way since they fulfill the contract indicated by the interface.

new LoginUser(new UserMongoDBRepository(), new GmailMailer());
new LoginUser(new UserMongoDBRepository(), new XMailer());

In the first case it will use the Gmail dependency, in the second case it will use the Xmail dependency.

With this article we have finished talking about the five SOLID principles. Using these principles with design patterns we will make a very good quality code, with a very good maintenance.

If you want to consult the other four SOLID principles, here are the links to each one:

  1. Single-responsibility Principle
  2. Open/Close Principle
  3. Liskov Substitution Principle
  4. Interface Segregation Principle

Frontend

Picture of Javier Motos

Javier Motos

Passionate about programming and learning every day. Currently focused on the frontend although I have always been in all parts of development.
Picture of Javier Motos

Javier Motos

Passionate about programming and learning every day. Currently focused on the frontend although I have always been in all parts of development.

We are HIRING!

What Can We Do