Los marcos de inyección de dependencia como Google Guice dan la siguiente motivación para su uso ( fuente ):
Para construir un objeto, primero construye sus dependencias. Pero para construir cada dependencia, necesita sus dependencias, y así sucesivamente. Entonces, cuando construyes un objeto, realmente necesitas construir un gráfico de objeto.
Construir gráficas de objetos a mano requiere mucha mano de obra (...) y dificulta las pruebas.
Pero no compro este argumento: incluso sin un marco de inyección de dependencias, puedo escribir clases que sean fáciles de instanciar y convenientes para probar. Por ejemplo, el ejemplo de la página de motivación de Guice podría reescribirse de la siguiente manera:
class BillingService
{
private final CreditCardProcessor processor;
private final TransactionLog transactionLog;
// constructor for tests, taking all collaborators as parameters
BillingService(CreditCardProcessor processor, TransactionLog transactionLog)
{
this.processor = processor;
this.transactionLog = transactionLog;
}
// constructor for production, calling the (productive) constructors of the collaborators
public BillingService()
{
this(new PaypalCreditCardProcessor(), new DatabaseTransactionLog());
}
public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard)
{
...
}
}
Por lo tanto, puede haber otros argumentos para los marcos de inyección de dependencia ( que están fuera del alcance de esta pregunta ), pero la creación sencilla de gráficos de objetos comprobables no es uno de ellos, ¿verdad?
new ShippingService()
.Respuestas:
Hay un viejo, viejo debate en curso sobre la mejor manera de hacer la inyección de dependencia.
El corte original del resorte instanciaba un objeto plano y luego inyectaba dependencias a través de métodos de establecimiento.
Pero luego una gran contingencia de personas insistió en que inyectar dependencias a través de parámetros de constructor era la forma correcta de hacerlo.
Luego, últimamente, a medida que el uso de la reflexión se hizo más común, establecer los valores de los miembros privados directamente, sin establecedores ni argumentos de constructor, se convirtió en la rabia.
Entonces, su primer constructor es consistente con el segundo enfoque para la inyección de dependencia. Le permite hacer cosas buenas como inyectar simulacros para probar.
Pero el constructor sin argumentos tiene este problema. Dado que está creando instancias de las clases de implementación para
PaypalCreditCardProcessor
yDatabaseTransactionLog
, crea una dependencia difícil en tiempo de compilación de PayPal y la Base de datos. Asume la responsabilidad de construir y configurar todo ese árbol de dependencias correctamente.Imagine que el procesador PayPay es un subsistema realmente complicado, y además incorpora muchas bibliotecas de soporte. Al crear una dependencia en tiempo de compilación en esa clase de implementación, está creando un enlace irrompible a todo ese árbol de dependencias. La complejidad de su gráfico de objeto acaba de aumentar en un orden de magnitud, tal vez dos.
Muchos de esos elementos en el árbol de dependencias serán transparentes, pero muchos de ellos también deberán ser instanciados. Lo más probable es que no puedas crear una instancia de a
PaypalCreditCardProcessor
.Además de la creación de instancias, cada uno de los objetos necesitará propiedades aplicadas desde la configuración.
Si solo tiene una dependencia en la interfaz y permite que una fábrica externa construya e inyecte la dependencia, se corta todo el árbol de dependencias de PayPal y la complejidad de su código se detiene en la interfaz.
Hay otros beneficios, como especificar clases de implementaciones en la configuración (es decir, en tiempo de ejecución en lugar de tiempo de compilación), o tener una especificación de dependencia más dinámica que varía, por ejemplo, por entorno (prueba, integración, producción).
Por ejemplo, supongamos que PayPalProcessor tenía 3 objetos dependientes, y cada una de esas dependencias tenía dos más. Y todos esos objetos tienen que extraer propiedades de la configuración. El código tal como está asumiría la responsabilidad de construir todo eso, establecer propiedades a partir de la configuración, etc. etc., todas las preocupaciones de las que se ocupará el marco DI.
Al principio puede no parecer obvio de qué se está protegiendo al usar un marco DI, pero se acumula y se vuelve dolorosamente obvio con el tiempo. (jaja, hablo desde la experiencia de haber intentado hacerlo de la manera difícil)
...
En la práctica, incluso para un programa realmente pequeño, encuentro que termino escribiendo en un estilo DI y divido las clases en pares de implementación / fábrica. Es decir, si no estoy usando un marco DI como Spring, solo agrego algunas clases simples de fábrica.
Eso proporciona la separación de las preocupaciones para que mi clase pueda hacer lo suyo, y la clase de fábrica asume la responsabilidad de construir y configurar cosas.
No es un enfoque obligatorio, pero FWIW
...
En términos más generales, el patrón DI / interfaz reduce la complejidad de su código al hacer dos cosas:
abstraer dependencias posteriores en interfaces
"levantar" dependencias ascendentes de su código y colocarlas en algún tipo de contenedor
Además de eso, dado que la creación de instancias y configuración de objetos es una tarea bastante familiar, el marco DI puede lograr muchas economías de escala a través de la notación estandarizada y el uso de trucos como la reflexión. Dispersar esas mismas preocupaciones alrededor de las clases termina agregando mucho más desorden de lo que uno pensaría.
fuente
Al vincular su gráfico de objeto al principio del proceso, es decir, conectarlo al código, requiere que tanto el contrato como la implementación estén presentes. Si alguien más (tal vez incluso usted) quiere usar ese código para un propósito ligeramente diferente, tiene que volver a calcular todo el gráfico del objeto y reimplementarlo.
Los marcos DI le permiten tomar un montón de componentes y conectarlos en tiempo de ejecución. Esto hace que el sistema sea "modular", compuesto por una serie de módulos que funcionan para las interfaces de los demás en lugar de las implementaciones de los demás.
fuente
El enfoque de "instanciar a mis propios colaboradores" puede funcionar para los árboles de dependencia , pero ciertamente no funcionará bien para los gráficos de dependencia que son gráficos acíclicos dirigidos en general (DAG). En un DAG de dependencia, varios nodos pueden apuntar al mismo nodo, lo que significa que dos objetos usan el mismo objeto como colaborador. De hecho, este caso no puede construirse con el enfoque descrito en la pregunta.
Si algunos de mis colaboradores (o los colaboradores de los colaboradores) compartieran un determinado objeto, necesitaría crear una instancia de este objeto y pasarlo a mis colaboradores. Entonces, de hecho, necesitaría saber más que mis colaboradores directos, y esto obviamente no escala.
fuente
UnitOfWork
que tiene unDbContext
repositorio singular y múltiple. Todos estos repositorios deberían usar el mismoDbContext
objeto. Con la "instanciación propia" propuesta por OP, eso se vuelve imposible de hacer.No he usado Google Guice, pero me he tomado una gran cantidad de tiempo migrando viejas aplicaciones de nivel N heredadas en arquitecturas .Net a IoC como Onion Layer que dependen de la Inyección de dependencia para desacoplar cosas.
¿Por qué la inyección de dependencia?
El propósito de la Inyección de dependencias no es en realidad la capacidad de prueba, sino tomar aplicaciones estrechamente acopladas y aflojar el acoplamiento tanto como sea posible. (Lo que tiene el deseable producto de hacer que su código sea mucho más fácil de adaptar para una prueba de unidad adecuada)
¿Por qué debería preocuparme por el acoplamiento?
El acoplamiento o las dependencias estrechas pueden ser algo muy peligroso. (Especialmente en lenguajes compilados) En estos casos, podría tener una biblioteca, dll, etc. que se usa muy raramente y que tiene un problema que efectivamente desconecta toda la aplicación. (Toda su aplicación muere porque una pieza sin importancia tiene un problema ... esto es malo ... REALMENTE malo) Ahora, cuando desacopla las cosas, puede configurar su aplicación para que pueda ejecutarse incluso si falta esa DLL o biblioteca por completo. Seguro que una pieza que necesita esa biblioteca o DLL no funcionará, pero el resto de la aplicación funciona felizmente.
¿Por qué necesito la inyección de dependencia para una prueba adecuada?
Realmente solo quieres un código débilmente acoplado, la Inyección de dependencias solo lo habilita. Puede acoplar cosas libremente sin IoC, pero generalmente es más trabajo y menos adaptable (estoy seguro de que alguien tiene una excepción)
En el caso que ha dado, creo que sería mucho más fácil configurar la inyección de dependencia para que pueda simular el código que no estoy interesado en contar como parte de esta prueba. Simplemente diga su método "Oye, sé que te dije que llamaras al repositorio, pero aquí están los datos que" deberían "devolver guiños " ahora porque esos datos nunca cambian, sabes que solo estás probando la parte que usa esos datos, no el recuperación real de los datos.
Básicamente, cuando realiza las pruebas, desea Pruebas de integración (funcionalidad) que prueban una pieza de funcionalidad de principio a fin, y pruebas de unidad completa que prueban cada pieza de código (generalmente a nivel de método o función) de forma independiente.
La idea es que quiere asegurarse de que toda la funcionalidad esté funcionando, si no quiere saber la parte exacta del código que no funciona.
Esto PUEDE hacerse sin la inyección de dependencia, pero generalmente a medida que su proyecto crece, se vuelve cada vez más engorroso sin la inyección de dependencia en su lugar. (¡SIEMPRE asuma que su proyecto crecerá! Es mejor tener una práctica innecesaria de habilidades útiles que encontrar un proyecto que se acelere rápidamente y requiera una refactorización y reingeniería seria después de que las cosas ya estén despegando).
fuente
Como mencioné en otra respuesta , el problema aquí es que desea que la clase
A
dependa de alguna claseB
sin codificar qué claseB
se usa en el código fuente de A. Esto es imposible en Java y C # porque la única forma de importar una clase es referirse a él por un nombre único globalmente.Al usar una interfaz, puede solucionar la dependencia de clase codificada, pero aún necesita tener en sus manos una instancia de la interfaz, y no puede llamar a los constructores o volverá al cuadrado 1. Así que ahora codifique que de otro modo podría crear sus dependencias, le quita esa responsabilidad a otra persona. Y sus dependencias están haciendo lo mismo. Entonces, cada vez que necesita una instancia de una clase, termina construyendo todo el árbol de dependencias manualmente, mientras que en el caso en que la clase A depende de B directamente, puede llamar
new A()
y hacer que esa llamada de constructornew B()
, y así sucesivamente.Un marco de inyección de dependencias intenta evitarlo al permitirle especificar las asignaciones entre clases y construir el árbol de dependencias por usted. El problema es que cuando arruinas las asignaciones, lo descubrirás en tiempo de ejecución, no en tiempo de compilación como lo harías en los lenguajes que admiten módulos de asignación como un concepto de primera clase.
fuente
Creo que este es un gran malentendido aquí.
Guice es un marco de inyección de dependencia . Hace DI automática . El punto que destacaron en el extracto que citó es acerca de que Guice puede eliminar la necesidad de crear manualmente ese "constructor comprobable" que presentó en su ejemplo. No tiene absolutamente nada que ver con la inyección de dependencia en sí.
Este constructor:
ya usa inyección de dependencia. Básicamente, acabas de decir que usar DI es fácil.
El problema que Guice resuelve es que para usar ese constructor ahora debe tener un código de constructor de gráfico de objeto en alguna parte , pasando manualmente los objetos ya instanciados como argumentos de ese constructor. Guice le permite tener un lugar único donde puede configurar qué clases de implementación reales corresponden a esas
CreditCardProcessor
eTransactionLog
interfaces. Después de esa configuración, cada vez que creaBillingService
usando Guice, esas clases se pasarán al constructor automáticamente.Esto es lo que hace el marco de inyección de dependencias . Pero el propio constructor que usted presentó ya es una implementación del principio de inyección de dependencia . Los contenedores IoC y los marcos DI son medios para automatizar los principios correspondientes, pero no hay nada que lo detenga para hacer todo a mano, ese fue el punto central.
fuente