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.
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. 😅
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.
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.
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.
¡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:
Si quieres aprender las bases de Java, toma el Curso de Introducción a Java SE.
Si quieres ir más allá y dominar los 4 pilares de la Programación Orientada a Objetos en Java, toma el Curso de Java SE Orientado a Objetos.
Si quieres estudiar a fondo los diferentes patrones de diseño que puedes implementar en JavaScript, toma el Curso Profesional de JavaScript.
Si quieres estudiar a fondo los principios SOLID y los patrones de diseño de los lenguajes de programación orientados a objetos, toma los cursos de Introducción a PHP, PHP con Laravel y PHP Avanzado.
Y por último, al menos esta vez, si quieres aprender a programar videojuegos con Unity3D, toma los cursos de C# para Videojuegos y Desarrollo de Videojuegos para Móviles con Unity.
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! 🤓💚
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
Para esto tenemos la Ruta de Aprendizaje de Desarrollo con Java: https://platzi.com/desarrollo-java/. 😉
Entonces como se resuelve este mismo problema con JS?