Logotipo SOLID

Terminamos los principios SOLID con el último de ellos, el Principio de Inversión de Dependencias, que viene a decir:

Los módulos de alto nivel no deberían depender de los módulos de bajo nivel. Ambos deberían depender de abstracciones.

Las abstracciones no deberían depender de los detalles. Los detalles deben depender de abstracciones.

Robert C. Martin ~ The Dependency Inversion Principle

Aunque parezca un trabalenguas, lo que viene a decir el tío Bob en esta ocasión es que las clases que utilizan otras no deberían depender de como funcionen. Estas a su vez, tendrán interfaces genéricas que no hagan depender a la clase de alto nivel de cada una.

Pero como siempre veamos todo con un ejemplo.

Imaginad una clase para compartir datos que en su inicio sólo sirva para compartir por Bluetooth:

class ShareAction {
  private bluetoothConnection: BluetoothConnection;

  ...

  private share(data: BluetoothStream, deviceId: string): void {
    this.bluetoothConnection.sendTo(data, deviceId);
  }
}

¿Qué pasa si queremos el día de mañana compartir por email, por ftp, etc? ¿Tener un atributo de cada tipo? ¿Consultar de forma externa añadiendo una dependencia no necesaria? Pues justo, con esto nos va a ayudar la inversión de dependencias. Lo primero que haríamos es crear una interfaz a la que poder llamar de una forma genérica:

interface IConnection {
  send(data: ArrayBuffer, destination: string): void;
}

El siguiente paso será pasar a nuestra clase la interfaz como parámetro, de forma que nuestra clase o (en la cita del principio, módulo de alto nivel) deja de depender de la clase que utiliza (módulo de bajo nivel). Así que recibirá la implementación específica de la interfaz que necesite en cada momento, sin tener que cambiar su contenido:

class ShareAction {
  private connection: IConnection;

  public constructor(connection: IConnection) {
    this.connection = connection;
  }

  private share(data: ArrayBuffer, destination: string): void {
    this.connection.send(data, destination);
  }
}

Pero... seguramente cada tipo de conexión no requiera exactamente los mismos datos, pues bien aquí entra en juego un segundo componente en juego, que serían los adaptadores (patrón adapter). Estos harían de interfaz entre cada tipo de conexión y la interfaz que tenemos, haciendo que "la abstracción no dependa de los detalles".

Y es que imaginad para este caso que con cada tipo de conexión (bluetooth, email, etc), usamos librerías. Cada librería tendrá una API distinta, y diferentes formas de poder enviar datos. Pues bien, crearemos un adaptador por cada una para que se pueden comunicar con nuestro sistema con la misma interfaz. Por ejemplo, en el caso de la conexión Bluetooth:

class BluetoothConnectionAdapter implements IConnection {
  private bluetoothConnection: BluetoothConnection;

  private send(data: ArrayBuffer, destination: string): void {
    const bluetoothStream = BluetoothConnectionAdapter.bufferToStream(data);
    this.bluetoothConnection.sendTo(bluetoothStream, destination);
  }
}

Esto quedaría con un esquema del tipo:

¿Y al final en qué se traduce todo esto? Pues que en nuestro ejemplo, hemos hecho que la clase ShareAction, al recibir sus dependencias del exterior deje de depender de la implementación de dichas clases. Permitiéndonos poder cambiar de un tipo de conexión a otra sin que ShareAction se vea modificada. Si el día de mañana se añade otro tipo de conexión, sólo hay que pasar en el constructor la referencia al nuevo adaptador y listo.

Por otro lado, no necesitamos las clases "reales" para testear, podemos usar mocks que pasaremos a la clase ShareAction, de forma que si hay un fallo en un test sabremos que es de la clase ShareAction y no de la dependencia que le hemos pasamdo.

Finalmente, cabe aclarar que la inyección de dependencias no es lo mismo que la inversión de dependencias. La primera es una herramienta con la que conseguir la segunda.

Podéis ver un resumen de todo lo que hemos visto en este artículo en este post de Instagram.

Conclusiones

Con este artículo hemos terminado de ver los principios SOLID. Quiero aclarar una cosilla para terminar, aunque los principios ofrecen ideas muy buenas, no siempre tienen que aplicarse desde el comienzo, sobre todo si no se sabe como evolucionará el proyecto; sino que se pueden aplicar cuando llegue el momento. Si tenemos una buena cobertura de test, refactorizar el código para adaptar algún principio no debería ser problema. Y es que en mi experiencia hay algunos casos que nunca cambian en la vida de un proyecto, por lo que aplicar desde el inicio estos principios puede resultar en una sobre-ingeniería que puede complicar de más el proyecto innecesariamente. Al final son herramientas que hay que saber cuando es el momento oportuno de usarlas, y eso se consigue con práctica y experiencia.

Espero que os haya gustado esta serie de artículos y ¡nos vemos en el próximo!


Más artículos de esta serie:
Capítulo S: Single Responsibility Principle
Capítulo O: Open/Closed Principle
Capítulo L: Liskov Substitution Principle
Capítulo I: Interface-Segregation Principle

Si quieres leer algo más de Robert C. Martin, aparte de su web, tiene varios libros interesantes:

Comparte este artículo con quien quieras
Principios SOLID. Capítulo I: Interface-Segregation Principle
5 cosas que haces mal en JavaScript

Leave a Comment

Your email address will not be published. Required fields are marked *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.