20

Aprende Inyección de Dependencias: El código es poder (Segunda parte)

111528Puntos

hace 2 años

Curso de Java SE Orientado a Objetos
Curso de Java SE Orientado a Objetos

Curso de Java SE Orientado a Objetos

Domina el estilo de programación que llevó a Java a ser un lenguaje ampliamente utilizado alrededor del mundo. Aplica y domina los conceptos de clases, objetos, herencia, abstracción, encapsulamiento y polimorfismo. Empieza a aprender hoy y domina los fundamentos de Java SE Orientado a Objetos.

La Inyección de Dependencias nos ayuda a cumplir con los últimos 2 principios SOLID: Interface Segregation y Dependency Inversion.

Este patrón de diseño consiste en cambiar un poco la lógica de nuestro código, de forma que en caso necesitar un método o atributo de alguna otra clase, no debamos llamar a esa clase directamente, sino escribir código lo suficientemente flexible como para que nos permita integrar (inyectar) cualquier otra parte de la aplicación.

Por supuesto, debemos seguir algunas reglas. Y de eso vamos a conversar en este artículo.

Recuerda que puedes (debes 😬) leer la primera parte de este artículo, así recordarás muy bien algunos conceptos: paradigmas de programación, POO y sus verdaderos objetivos, patrones de diseño, buenas prácticas, SOLID, entre otros.

Inyección de Dependencias

Injección de Dependencias

La Inyección de Dependencias nos ayuda a cumplir con los últimos 2 principios SOLID: Interface Segregation y Dependency Inversion. 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.

No te preocupes. Todo va a quedar muy claro con los ejemplos. ¡Paciencia!

Recuerda que la implementación de este patrón de diseño es un poco diferente en cada lenguaje. En JavaScript no debemos preocuparnos por interfaces o tipos de datos, ya que eso no existe en JavaScript. 😅

Mind Blow

Pero no te preocupes, estos cambios no nos impiden implementar este patrón de diseño en lenguajes tan hermosos e interesantes como JavaScript. Simplemente, la forma de hacerlo es un poquito diferente. En esta ocasión vamos a estudiar la inyección de dependencias en Java.

¿Por qué en Java? Esa no es la pregunta. Las preguntas correctas son: ¿Por qué JavaScript suena tan parecido a Java? ¿Por qué existen personas malas en el mundo? ¿Cuál es nuestro propósito en la vida? ¿Qué nos detiene de seguir aprendiendo a pesar de las dificultades?

No te preocupes, atractiva personita que por ahora solo conoce JavaScript. Perder el miedo a Java y aprender un nuevo lenguaje de programación vale totalmente la pena (aprender nuevos lenguajes no es normal en mí, pero sí que lo es en una comunidad tan espectacular como Platzi 💚).

Para que no te pierdas, haré un breve resumen de cómo funcionan todos estos términos raros que acabas de leer. A cambio, solo te pido que leas y comentes este otro artículo sobre Cómo perder el miedo a Java si eres JS Developer.

Resumen ultra rápido: Tipos de Datos e Interfaces

En realidad, tú ya conoces los Tipos de Datos. Son todos los tipos de variables y código que puedes programar: strings, números, booleanos, arrays, etc. Listo, eso es un tipo de dato.

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 (por lo menos yo) podemos estar muy acostumbrado 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, listo. 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 de acostumbrarse.

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

Las interfaces funcionan muy parecido a heredar clases común y corrientes. Solo que, en vez de heredar métodos con sus respectivos comportamientos, definimos el nombre del método y listo.

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

¡Cierto! Lo prometido es deuda. A continuación, más cursos para continuar tu ruta de aprendizaje y dominar las tecnologías 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! 🤓💚

Curso de Java SE Orientado a Objetos
Curso de Java SE Orientado a Objetos

Curso de Java SE Orientado a Objetos

Domina el estilo de programación que llevó a Java a ser un lenguaje ampliamente utilizado alrededor del mundo. Aplica y domina los conceptos de clases, objetos, herencia, abstracción, encapsulamiento y polimorfismo. Empieza a aprender hoy y domina los fundamentos de Java SE Orientado a Objetos.
Juan David
Juan David
juandc

111528Puntos

hace 2 años

Todas sus entradas
Escribe tu comentario
+ 2
4
15951Puntos

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