36

Inyección de Dependencias vs. Inversión de Dependencias: ¿cuál es la diferencia?

211751Puntos

hace 5 años

La inversión de dependencias es uno de los 5 principios SOLID de la programación orientada a objetos. Y la inyección de dependencias es un patrón de diseño que nos permite implementar 2 de los principios SOLID: Interface Segregation y Dependency Inversion.

En esta lectura profundizaremos en cómo implementar el patrón de inyección de dependencias. Si quieres aprender un poco más sobre SOLID y la inversión de dependencias, te recomiendo la lectura de buenas prácticas para desarrollo de software.

Inyección de Dependencias

Inyección de Dependencias

La inyección de dependencias consiste en cambiar la lógica de nuestro código para que en caso necesitar un método o atributo de alguna otra clase, no debamos llamar a esa clase directamente, sino adecuar nuestras clases para que sean flexibles y nos permitan integrar (inyectar) cualquier otra clase que siga unas ciertas reglas.

Recuerda que la implementación de este patrón de diseño es un poco diferente en cada lenguaje. En esta ocasión vamos a estudiar la inyección de dependencias en Java.

💡 Cómo perder el miedo a Java si eres JS Developer.

Tipos de Datos

Los tipos de datos son todos los tipos de variables que puedes programar: strings, números, booleanos, arrays…

La diferencia entre lenguajes fuertemente tipados como Java y otros débilmente tipados como JavaScript es cuánto te “obligan” a definir el tipo de dato de cada variable que declaras. Si tu variable es un String, debes aclararlo. Si es un número entero, también debes indicarlo en el código.

Esto podría no significar mucho trabajo, ya que cada vez que creas una variable sabes exactamente qué tipo de variable vas a declarar.

int a = 30; // números enterosdouble b = 2.65; // números con decimaleschar c = ‘H’; // un solo carácter
String d = “Esto es un string”; // texto tan largo como quieras

El problema es que podemos estar muy acostumbrados a cambiar el tipo de dato de una variable como si nada, cosa que los lenguajes tipados no nos permiten, por lo menos no de la forma en la que estamos acostumbrados.

En JavaScript los números son números “y ya”. Pero los lenguajes tipados usan muy diferente los números enteros y los decimales, por lo que, al realizar una división inexacta entre números enteros, adivina: el resultado también será un número entero, a pesar de que el resultado incluya decimales.

// 30/4 -> 7.5int a = 30;
int b = 4;

a/b = 7// what!???

Esto se puede solucionar con casteo, afortunadamente. Pero si no estás acostumbrado a este tipo de prácticas puede resultar un poco confuso. Solo es cuestión de acostumbrarse.

Interfaces

Las Interfaces son muy parecidas a los tipos de datos, pero para clases. Son moldes donde definimos los métodos y comportamientos que un conjunto de clases debe implementar sí o sí.

Las interfaces funcionan muy parecido a la herencia de clases. Solo que, en vez de heredar métodos con sus respectivos comportamientos, definimos el nombre y tipos del método, más no su implementación interna.

publicinterfaceMétodosObligatorios{
  publicvoid métodoObligatorio1(String message);
  publicvoid métodoObligatorio2(int number);
}

publicclassClaseUsandoInterfacesimplementsMétodosObligatorios{
  publicvoid métodoObligatorio1(String message) {
    System.out.println(“Implementación del método obligatorio 1. ”);
  }

  publicvoid métodoObligatorio2(int number) {
      System.out.println(“Implementación del método obligatorio 2”);
  }
}

publicclassOtraClaseQueUsaInterfacesimplementsMétodosObligatorios{
  publicvoid métodoObligatorio1(String message) {
    System.out.println(“Otra implementación del método obligatorio 1. ”);
  }

  publicvoid métodoObligatorio2(int number) {
      System.out.println(“Otra implementación del método obligatorio 2”);
  }
}

¿Para qué nos sirven entonces las interfaces?

La respuesta no podría ser más clara. En mi opinión, el mejor uso que tienen las interfaces es ayudarnos a implementar Inyección de Dependencias.

Usando la Inyección de Dependencias en Java

Nuestra aplicación será un videojuego muy parecido a InfinityBrayan. Como sabes, existen diferentes dispositivos donde podemos jugar: XBOX, PlayStation, nuestras computadoras, celulares, etc. Nuestro trabajo es que las clases de los juegos sean lo suficientemente flexibles como para soportar cualquiera de estos dispositivos.

Por ahora, las diferentes clases de nuestros juegos solo pueden jugarse desde una computadora. El código se ve más o menos así:

publicclassInfinityBrayan{
  // OJO AQUÍ: los controles son de tipo Computer...private Computer controls;

  publicvoidInfinityBrayan(){
    this.controls = new Computer();
  }

  publicvoidrun(char direction){
    switch(direction) {
      // la clase Computer tiene (o debe tener) los// métodos moveWhatever...case ‘U’:
        this.controls.moveUp();
        break;
      case ‘D’:
        this.controls.moveDown();
        break;
      case ‘L’:
        this.controls.moveLeft();
        break;
      case ‘R’:
        this.controls.moveRight();
        break;
    }
  }

  publicintgetPositionX(){
    returnthis.controls.positionX;
  }

  publicintgetPositionY(){
    returnthis.controls.positionY;
  }
}

publicclassComputer{
  publicint positionX = 0;
  publicint positionY = 0;

  publicvoidmoveUp(){
    this.positionY++;
  }

  publicvoidmoveDown(){
    this.positionY--;
  }

  publicvoidmoveLeft(){
    this.positionX--;
  }

  publicvoidmoveRight(){
    this.positionX++;
  }
}

Nuestra clase InfinityBrayan llama directamente a la clase Computer para acceder a sus métodos de mover al jugador o detectar su posición. El problema es que este nivel de acoplamiento nos impide integrar los controles de cualquier otro dispositivo, ya que no tenemos forma de indicarle al videojuego cuál de ellos debe usar.

Existen diferentes formas de solucionarlo. Por ejemplo: podríamos crear una clase InfinityBrayan para cada dispositivo que nuestro juego deba soportar.

// Videojuego que funciona ÚNICAMENTE en computadoras:publicclassInfinityBrayan4PC{
  private Computer controls;

  publicvoidInfinityBrayan(){
    this.controls = new Computer();
  }

  publicvoidrun(char direction){ /* ... */ }

  publicintgetPositionX(){ /* ... */ }
  publicintgetPositionY(){ /* ... */ }
}

// Videojuego que funciona ÚNICAMENTE en PlayStation:publicclassInfinityBrayan4PS{
  private PlayStation controls;

  publicvoidInfinityBrayan(){
    this.controls = new PlayStation();
  }

  publicvoidrun(char direction){ /* ... */ }

  publicintgetPositionX(){ /* ... */ }
  publicintgetPositionY(){ /* ... */ }
}

// Dispositivos:publicclassComputer{
  publicint positionX = 0;
  publicint positionY = 0;

  publicvoidmoveUp(){ /* ... */ }
  publicvoidmoveDown(){ /* ... */ }
  publicvoidmoveLeft(){ /* ... */ }
  publicvoidmoveRight(){ /* ... */ }
}

publicclassPlayStation{
  publicint positionX = 0;
  publicint positionY = 0;

  publicvoidmoveUp(){ /* ... */ }
  publicvoidmoveDown(){ /* ... */ }
  publicvoidmoveLeft(){ /* ... */ }
  publicvoidmoveRight(){ /* ... */ }
}

¡El problema de esta “solución” es que repetimos código como locos!

Fíjate muy bien. Lo único que cambia entre las diferentes versiones de nuestros juegos es el dispositivo que usan para acceder a los controles de movimiento, el resto es absolutamente igual.

Para solucionar todos estos problemas está la inimaginablemente espectacular inyección de dependencias. En vez de crear una clase para cada dispositivo que podamos soportar con nuestro videojuego, vamos a indicarle a la instancia controls que debe ser extremadamente flexible, ya que debe adoptar la forma del dispositivo que le indiquemos por medio de argumentos.

¡Vamos por pasos!

¿Recuerdas las interfaces? Como pudiste darte cuenta, todos los dispositivos implementan los mismos métodos para mover al jugador. Por lo tanto, podemos crear una interfaz que defina las reglas que deben seguir todos los dispositivos que pueda soportar nuestro juego.

publicinterfaceMoveControlDevices{
  publicint positionX;
  publicint positionY;

  publicvoidmoveUp();
  publicvoidmoveDown();
  publicvoidmoveLeft();
  publicvoidmoveRight();
}

Con la interfaz lista podemos definir que todos nuestros dispositivos deben implementarla, así nos aseguramos de que estos dispositivos están listos para mover al jugador y funcionar perfectamente en nuestro juego.

publicclassComputerimplementsMoveControlDevices{
  // ...
}

publicclassPlayStationimplementsMoveControlDevices{
  // ...
}

Ya sabemos que todos los dispositivos tienen los mismos métodos y atributos obligatorios que definimos en la interfaz MoveControlDevices. Por lo tanto, el tipo de dato de nuestros controles puede ser de tipo MoveControlDevices. De esta forma evitamos la validación de cada tipo de dispositivo, ya que todas siguen las mismas reglas: las de la interfaz MoveControlDevices.

publicclassInfinityBrayan4AllDevices{
  // los controles pueden ser de tipo Computer, PlayStation o cualquier// otra clase que siga las reglas de MoveControlDevices...private MoveControlDevices controls;

  publicvoidInfinityBrayan4AllDevices(){
    // ...
  }
}

Pero eso no es todo. Aún debemos encontrar una forma de indicarle al videojuego qué dispositivo queremos usar.

Para esto vamos a definir un nuevo parámetro en el constructor del juego. El parámetro device nos indicará el dispositivo que vamos a usar, solo debemos pasarle la instancia del dispositivo que queremos al momento de instanciar nuestro videojuego.

publicclassInfinityBrayan4AllDevices{
  private MoveControlDevices controls;

  publicvoidInfinityBrayan4AllDevices(MoveControlDevices device){
    this.controls = device;
  }
}

MoveControlDevices computer = new Computer();
InfinityBrayan4AllDevices infinityBrayan4PC = InfinityBrayan4AllDevices(computer);

MoveControlDevices playStation = new PlayStation();
InfinityBrayan4AllDevices infinityBrayan4PS = InfinityBrayan4AllDevices(playStation);

De esta forma hemos usado la inyección de dependencias para conseguir que nuestra clase InfinityBrayan soporte cualquier dispositivo que siga las reglas que definimos en la interfaz MoveControlDevices.

Puedes encontrar el código completo en el siguiente repositorio: github.com/juandc/java-dependency-injection-example.

Conclusiones

A continuación, cursos donde puedes continuar tu ruta de aprendizaje para dominar las tecnologías y buenas prácticas correctas para programar tu próximo proyecto:

En los comentarios encontrarás un video para repasar las soluciones a diferentes problemas que nos ayuda a obtener la Inyección de Dependencias. 😉

¡#NuncaParesDeAprender! 🤓💚

Juan
Juan
juandc

211751Puntos

hace 5 años

Todas sus entradas
Escribe tu comentario
+ 2
Ordenar por:
4
23952Puntos

Sería bueno tener una escuela de Java y aprender todo sobre Java EE, puesto que muchos patrones de diseños son muy buenos y me gustaría aprender lo más que puedo pero aquí en Platzi

1
6437Puntos

Entonces como se resuelve este mismo problema con JS?