
En este artículo, hablaré sobre los conceptos básicos de la inyección de dependencia (Ing. Dependency Injection, DI ) en un lenguaje simple, y también sobre las razones para usar este enfoque. Este artículo está dirigido a aquellos que no saben qué es la inyección de dependencia o que dudan de la necesidad de utilizar esta técnica. Entonces comencemos.
¿Qué es la adicción?
Veamos primero un ejemplo. Tenemos ClassA
, ClassB
y ClassC
como se muestra a continuación:
class ClassA { var classB: ClassB } class ClassB { var classC: ClassC } class ClassC { }
Puede ver que la clase ClassA
contiene una instancia de la clase ClassB
, por lo que podemos decir que la clase ClassA
depende de la clase ClassB
. Por qué Porque ClassA
necesita ClassB
para funcionar correctamente. También podemos decir que la clase ClassB
es una dependencia de la clase ClassA
.
Antes de continuar, quiero aclarar que tal relación es buena, porque no necesitamos una clase para hacer todo el trabajo en la aplicación. Necesitamos dividir la lógica en diferentes clases, cada una de las cuales será responsable de una determinada función. Y en este caso, las clases podrán interactuar de manera efectiva.
¿Cómo trabajar con dependencias?
Veamos tres métodos que se utilizan para realizar tareas de inyección de dependencia:
Primera forma: crear dependencias en una clase dependiente
En pocas palabras, podemos crear objetos siempre que los necesitemos. Eche un vistazo al siguiente ejemplo:
class ClassA { var classB: ClassB fun someMethodOrConstructor() { classB = ClassB() classB.doSomething() } }
Es muy facil! Creamos una clase cuando la necesitamos.
Los beneficios
- Es facil y sencillo.
- La clase dependiente (
ClassA
en nuestro caso) controla completamente cómo y cuándo crear las dependencias.
Desventajas
ClassA
y ClassB
estrechamente relacionadas entre sí. Por lo tanto, cada vez que necesitemos usar ClassA
, nos veremos obligados a usar ClassB
, y será imposible reemplazar ClassB
con otra cosa .- Con cualquier cambio en la inicialización de la clase
ClassB
, deberá ajustar el código dentro de la clase ClassA
(y todas las demás clases dependientes de ClassB
). Esto complica el proceso de cambiar la dependencia. ClassA
no puede ser probada. Si necesita probar una clase y, sin embargo, este es uno de los aspectos más importantes del desarrollo de software, deberá realizar pruebas unitarias de cada clase por separado. Esto significa que si desea verificar el funcionamiento correcto de la clase ClassA
y crear varias pruebas unitarias para verificarlo, entonces, como se muestra en el ejemplo, en cualquier caso también creará una instancia de la clase ClassB
, incluso cuando no le interese. Si se produce un error durante la prueba, no podrá comprender dónde se encuentra: en ClassA
o ClassB
Después de todo, existe la posibilidad de que parte del código en ClassB
provocó un error, mientras que ClassA
funciona correctamente. En otras palabras, las pruebas unitarias no son posibles porque los módulos (clases) no se pueden separar entre sí.ClassA
debe configurarse de modo que pueda inyectar dependencias. En nuestro ejemplo, necesita saber cómo crear un ClassC
y usarlo para crear un ClassB
. Sería mejor si no supiera nada al respecto. Por qué Debido al principio de responsabilidad única .
Cada clase solo debe hacer su trabajo.
Por lo tanto, no queremos que las clases sean responsables de otra cosa que no sean sus propias tareas. La implementación de dependencias es una tarea adicional que establecemos para ellos.
Segunda forma: inyectar dependencias a través de una clase personalizada
Entonces, entender que inyectar dependencias dentro de una clase dependiente no es una buena idea, exploremos una forma alternativa. Aquí, la clase dependiente define todas las dependencias que necesita dentro del constructor y permite que la clase de usuario las proporcione. ¿Es esta una solución a nuestro problema? Lo sabremos un poco más tarde.
Eche un vistazo al código de muestra a continuación:
class ClassA { var classB: ClassB constructor(classB: ClassB){ this.classB = classB } } class ClassB { var classC: ClassC constructor(classC: ClassC){ this.classC = classC } } class ClassC { constructor(){ } } class UserClass(){ fun doSomething(){ val classC = ClassC(); val classB = ClassB(classC); val classA = ClassA(classB); classA.someMethod(); } } view rawDI Example In Medium -
Ahora ClassA
obtiene todas las dependencias dentro del constructor y simplemente puede llamar a los métodos de la clase ClassB
sin inicializar nada.
Los beneficios
ClassA
y la ClassB
ahora ClassB
acopladas de manera flexible, y podemos reemplazar a la ClassB
sin romper el código dentro de la ClassA
Por ejemplo, en lugar de pasar ClassB
podemos pasar AssumeClassB
, que es una subclase de ClassB
, y nuestro programa funcionará correctamente.ClassA
ahora se puede probar. Al escribir una prueba unitaria, podemos crear nuestra propia versión de ClassB
(objeto de prueba) y pasarla a ClassA
. Si se produce un error al pasar la prueba, ahora sabemos con certeza que este es definitivamente un error en la ClassA
ClassB
libre de trabajar con dependencias y puede centrarse en sus tareas.
Desventajas
- Este método se asemeja a un mecanismo de cadena, y en algún momento la cadena debería interrumpirse. En otras palabras, el usuario de la clase
ClassA
debe saber todo sobre la inicialización de ClassB
, lo que a su vez requiere conocimiento sobre la inicialización de ClassC
, etc. Entonces, verá que cualquier cambio en el constructor de cualquiera de estas clases puede conducir a un cambio en la clase de llamada, sin mencionar que ClassA
puede tener más de un usuario, por lo que se repetirá la lógica de crear objetos. - A pesar de que nuestras dependencias son claras y fáciles de entender, el código de usuario no es trivial y es difícil de administrar. Por lo tanto, no todo es tan simple. Además, el código viola el principio de responsabilidad única, ya que es responsable no solo de su trabajo, sino también de la implementación de dependencias en clases dependientes.
El segundo método obviamente funciona mejor que el primero, pero aún tiene sus defectos. ¿Es posible encontrar una solución más adecuada? Antes de considerar la tercera vía, hablemos primero sobre el concepto mismo de inyección de dependencia.
¿Qué es la inyección de dependencia?
La inyección de dependencias es una forma de manejar dependencias fuera de la clase dependiente cuando la clase dependiente no necesita hacer nada.
Basado en esta definición, nuestra primera solución obviamente no usa la idea de inyección de dependencia, y la segunda forma es que la clase dependiente no hace nada para proporcionar las dependencias. Pero aún creemos que la segunda solución es mala. POR QUÉ?
Dado que la definición de inyección de dependencia no dice nada sobre dónde debería llevarse a cabo el trabajo con dependencias (excepto fuera de la clase dependiente), el desarrollador debe elegir un lugar adecuado para la inyección de dependencia. Como puede ver en el segundo ejemplo, la clase de usuario no es el lugar correcto.
¿Cómo hacerlo mejor? Veamos una tercera forma de manejar dependencias.
Tercera forma: deje que otra persona maneje las dependencias en lugar de nosotros
Según el primer enfoque, las clases dependientes son responsables de obtener sus propias dependencias, y en el segundo enfoque, trasladamos el procesamiento de dependencias de la clase dependiente a la clase de usuario. Imaginemos que hay alguien más que podría manejar las dependencias, como resultado de lo cual ni el dependiente ni las clases de usuario harían el trabajo. Este método le permite trabajar con dependencias en la aplicación directamente.
Una implementación "limpia" de inyección de dependencia (en mi opinión personal)
La responsabilidad del manejo de dependencias recae en un tercero, por lo que ninguna parte de la aplicación interactuará con ellos.
La inyección de dependencia no es una tecnología, un marco, una biblioteca o algo así. Esto es solo una idea. La idea es trabajar con dependencias fuera de la clase dependiente (preferiblemente en una parte especialmente asignada). Puede aplicar esta idea sin usar bibliotecas o marcos. Sin embargo, generalmente recurrimos a marcos para implementar dependencias, porque simplifica el trabajo y evita escribir código de plantilla.
Cualquier marco de inyección de dependencia tiene dos características inherentes. Otras funciones adicionales pueden estar disponibles para usted, pero estas dos funciones siempre estarán presentes:
En primer lugar, estos marcos ofrecen una forma de determinar los campos (objetos) que deben implementarse. Algunos marcos hacen esto anotando un campo o constructor usando la anotación @Inject
, pero hay otros métodos. Por ejemplo, Koin utiliza las funciones de lenguaje integradas de Kotlin para determinar la implementación. Inject
significa que la dependencia debe ser manejada por el marco DI. El código se verá así:
class ClassA { var classB: ClassB @Inject constructor(classB: ClassB){ this.classB = classB } } class ClassB { var classC: ClassC @Inject constructor(classC: ClassC){ this.classC = classC } } class ClassC { @Inject constructor(){ } }
En segundo lugar, los marcos le permiten determinar cómo proporcionar cada dependencia, y esto sucede en un archivo (s) separado (s). Aproximadamente se ve así (tenga en cuenta que esto es solo un ejemplo, y puede diferir de un marco a otro):
class OurThirdPartyGuy { fun provideClassC(){ return ClassC()
Entonces, como puede ver, cada función es responsable de procesar una dependencia. Por lo tanto, si necesitamos usar ClassA
en algún lugar de la aplicación, sucederá lo siguiente: nuestro marco DI crea una instancia de la clase ClassC
llamando a provideClassC
, pasándola a provideClassB
y recibiendo una instancia de ClassB
, que se pasa a provideClassA
, y como resultado, se crea ClassA
. Esto es casi mágico. Ahora examinemos las ventajas y ventajas del tercer método.
Los beneficios
- Todo es lo más simple posible. Tanto la clase dependiente como la clase que proporciona las dependencias son claras y simples.
- Las clases están unidas libremente y son fácilmente reemplazables por otras clases. Supongamos que queremos reemplazar
ClassC
con AssumeClassC
, que es una subclase de ClassC
. Para hacer esto, solo necesita cambiar el código del proveedor de la siguiente manera, y donde ClassC
se use ClassC
, la nueva versión ahora se usará automáticamente:
fun provideClassC(){ return AssumeClassC() }
Tenga en cuenta que ningún código dentro de la aplicación cambia, solo el método del proveedor. Parece que nada podría ser aún más simple y más flexible.
- Increíble capacidad de prueba. Puede reemplazar fácilmente las dependencias con versiones de prueba durante la prueba. De hecho, la inyección de dependencia es su principal ayuda cuando se trata de pruebas.
- Mejora de la estructura del código, como la aplicación tiene un lugar separado para el procesamiento de dependencia. Como resultado, el resto de la aplicación puede enfocarse exclusivamente en sus funciones y no superponerse con dependencias.
Desventajas
- Los marcos DI tienen un cierto umbral de entrada, por lo que el equipo del proyecto necesita pasar tiempo y estudiarlo antes de usarlo de manera efectiva.
Conclusión
- El manejo de dependencias sin DI es posible, pero puede provocar bloqueos en la aplicación.
- DI es solo una idea efectiva, según la cual es posible manejar dependencias fuera de la clase dependiente.
- Es más efectivo usar DI en ciertas partes de la aplicación. Muchos marcos contribuyen a esto.
- Los marcos y las bibliotecas no son necesarios para DI, pero pueden ayudar mucho.
En este artículo, traté de explicar los conceptos básicos del trabajo con el concepto de inyección de dependencia, y también enumeré las razones para usar esta idea. Hay muchos más recursos que puede explorar para obtener más información sobre el uso de DI en sus propias aplicaciones. Por ejemplo, una sección separada en la parte avanzada de nuestro curso de profesión de Android está dedicada a este tema.