Principios SOLID (5): Dependency inversion principle

Para acabar con los 5 principios SOLID, hoy vamos a hablar del último de estos, el “Dependency Inversion principle”

Dependency Inversion Principle

En la década de los 90, Robert C.Martin (conocido coloquialmente como Uncle Bob) formuló el quinto principio de los 5 principios SOLID. El principio conocido como “Dependence Inversion” o “Inversión de dependencias»). 

dependency_inversion_principle

¿Qué es el principio de “Dependency Inversion Principle”?

El principio recomienda: 

  • Los módulos de alto nivel no deben depender de los módulos de bajo nivel. Ambos deben depender de abstracciones (interfaces).
  • Las abstracciones no deberían depender de los detalles. Los detalles deberían depender de las abstracciones.

¿Cómo detectar que estamos violando el principio de “Dependency Inversion”?

Una de las formas más sencillas de detectar si estamos violando el principio es evaluar si estamos dependiendo de módulos de bajo nivel en un módulo de alto nivel. 

Otra señal que debería llamar nuestra atención es si apreciamos que, en nuestros módulos de alto nivel, no disponemos de interfaces o tenemos falta de ellas.

¿Qué ocurre si cambiamos un módulo de bajo nivel? Si al responder esta pregunta nos damos cuenta que para realizar el cambio de un módulo de bajo nivel hay que realizar cambios en el código del módulo de alto nivel, es que no estamos utilizando el principio correctamente. 

Otro punto importante para la detección de la violación son las pruebas unitarias (unit tests). Si al realizar la pruebas unitarias nos damos cuenta que tienen una gran complejidad debido a las dependencias podemos empezar a sospechar que estamos violando el principio.

¿Cómo solucionar la violación del principio?

La mejor forma de solucionar la violación del principio es crear interfaces y abstracciones. Con esto, los módulos de alto nivel utilizan implementaciones de las clases de bajo nivel. Utilizando estas implementaciones dejamos de depender de los módulos de bajo nivel y por lo tanto dejamos de violar el quinto de los principios SOLID. 

Ejemplo:

Imaginemos que tenemos un servicio de login de usuario. Este servicio tiene que comprobar si el usuario existe en bbdd y enviar un email enviando un enlace de acceso a la cuenta.

Este servicio podría ser algo similar a esto:

class LoginUser {
  private repository;
  private mailer;

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

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

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

Aquí estamos directamente utilizando el repositorio de Mongo y la librería de Gmail directamente, es decir, el servicio está dependiendo de módulos de bajo nivel, no de implementaciones.

Esto nos complicaría muchísimo el testing unitario. Además, si quisiéramos cambiar el módulo de bajo nivel (por ejemplo, cambiar Gmail por otra plataforma de envío de emails) tendríamos que modificar nuestro código.

Para evitar esto, podemos crear dos interfaces, una para el acceso a la base de datos y otra para el envío de correos. Estas interfaces podrían ser algo así:

interface UserRepository {
  find(email: string);
}

interface Mailer {
  send(email: string)
}

Una vez tenemos nuestras dos interfaces, lo que tenemos que crear son los servicios que vayan a implementarlas:

class UserMongoDBRepository implements UserRepository {
  find(email: string) {
    // Acceso a mongo para la obtención del usuario (método getUser)
  }
}

class GmailMailer implements Mailer {
  send(email: string) {
    // Acceso al api de GMAIL para enviar los datos (método sendLoginEmail)
  }
}

Con estas implementaciones de las interfaces estamos creando una capa de abstracción, ya que si se quisiera cambiar el método de envío de emails, tendríamos que hacer lo siguiente:

class XMailer implements Mailer {
  send(email: string) {
    // Acceso al api de X para enviar los datos (p.e: método sendMailX)
  }
}

El servicio de login utilizando las implementaciones quedaría de la siguiente manera:

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

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

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

En el punto donde se instancia el servicio de LoginUser, podemos indicar qué implementaciones queremos utilizar. Como todos los servicios implementan una interfaz, conseguimos que todo el código funcione de una forma correcta ya que cumplen el contrato que indica la interfaz.

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

En el primer caso usará la dependencia de Gmail, en el segundo de Xmail.

Con este artículo hemos terminado de hablar de los cinco principios SOLID. Utilizando estos principios con patrones de diseños realizaremos un código de una calidad muy buena, con un muy buen mantenimiento.

Si quieres consultar los otros cuatro principios SOLID, te dejo a continuación los enlaces de cada uno:

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

Frontend

Picture of Javier Motos

Javier Motos

Apasionado de la programación y de aprender cada día. Actualmente centrado en el frontend aunque siempre he estado en todas las partes del desarrollo.
Picture of Javier Motos

Javier Motos

Apasionado de la programación y de aprender cada día. Actualmente centrado en el frontend aunque siempre he estado en todas las partes del desarrollo.

We are HIRING!

What Can We Do