La inyección de dependencia (DI) es un patrón bien conocido y de moda. La mayoría de los ingenieros conocen sus ventajas, como:
- Hacer que el aislamiento en las pruebas unitarias sea posible / fácil
- Definición explícita de dependencias de una clase
- Facilitar un buen diseño ( principio de responsabilidad única (SRP) por ejemplo)
- Habilitar las implementaciones de conmutación rápidamente (en
DbLogger
lugar de,ConsoleLogger
por ejemplo)
Creo que existe un amplio consenso en la industria de que la DI es un patrón bueno y útil. No hay demasiadas críticas en este momento. Las desventajas que se mencionan en la comunidad son generalmente menores. Algunos:
- Mayor número de clases.
- Creación de interfaces innecesarias.
Actualmente discutimos el diseño de arquitectura con mi colega. Es bastante conservador, pero de mente abierta. Le gusta cuestionar cosas, lo que considero bueno, porque muchas personas en TI simplemente copian la última tendencia, repiten las ventajas y, en general, no piensan demasiado, no analizan demasiado.
Las cosas que me gustaría preguntar son:
- ¿Deberíamos usar la inyección de dependencia cuando solo tenemos una implementación?
- ¿Deberíamos prohibir la creación de nuevos objetos, excepto los de lenguaje / marco?
- ¿Inyectar una sola implementación es una mala idea (digamos que tenemos solo una implementación, por lo que no queremos crear una interfaz "vacía") si no planeamos probar la unidad en una clase en particular?
UserService
clase, esa es solo un titular de lógica. Se inyecta una conexión de base de datos y se ejecutan pruebas dentro de una transacción que se revierte. Muchos llamarían a esto una mala práctica, pero descubrí que esto funciona extremadamente bien. No necesita contorsionar su código solo para realizar pruebas y obtendrá el error de encontrar el poder de las pruebas de integración.Respuestas:
Primero, me gustaría separar el enfoque de diseño del concepto de marcos. La inyección de dependencia en su nivel más simple y fundamental es simplemente:
Eso es. Tenga en cuenta que nada de eso requiere interfaces, marcos, cualquier estilo de inyección, etc. Para ser justos, supe por primera vez sobre este patrón hace 20 años. No es nuevo
Debido a que más de 2 personas tienen confusión sobre el término padre e hijo, en el contexto de la inyección de dependencia:
La inyección de dependencia es un patrón para la composición de objetos .
¿Por qué las interfaces?
Las interfaces son un contrato. Existen para limitar qué tan estrechamente unidos pueden estar dos objetos. No todas las dependencias necesitan una interfaz, pero ayudan a escribir código modular.
Cuando agrega el concepto de pruebas unitarias, puede tener dos implementaciones conceptuales para cualquier interfaz dada: el objeto real que desea usar en su aplicación y el objeto burlado o tropezado que usa para probar el código que depende del objeto. Eso solo puede ser una justificación suficiente para la interfaz.
¿Por qué marcos?
Esencialmente, inicializar y proporcionar dependencias a objetos secundarios puede ser desalentador cuando hay una gran cantidad de ellos. Los marcos proporcionan los siguientes beneficios:
También tienen las siguientes desventajas:
Dicho todo esto, hay compensaciones. Para proyectos pequeños donde no hay muchas partes móviles, y hay pocas razones para usar un marco DI. Sin embargo, para proyectos más complicados donde ya hay ciertos componentes hechos para usted, el marco puede estar justificado.
¿Qué pasa con [artículo aleatorio en Internet]?
¿Qué hay de eso? Muchas veces las personas pueden ponerse demasiado celosas y agregar un montón de restricciones y regañarte si no estás haciendo las cosas de la "manera verdadera". No hay una sola forma verdadera. Vea si puede extraer algo útil del artículo e ignore las cosas con las que no está de acuerdo.
En resumen, piensa por ti mismo y prueba cosas.
Trabajando con "viejos jefes"
Aprende todo lo que puedas. Lo que encontrará con muchos desarrolladores que están trabajando en sus 70 años es que han aprendido a no ser dogmáticos sobre muchas cosas. Tienen métodos con los que han trabajado durante décadas que producen resultados correctos.
He tenido el privilegio de trabajar con algunos de estos, y pueden proporcionar algunos comentarios brutalmente honestos que tienen mucho sentido. Y donde ven valor, agregan esas herramientas a su repertorio.
fuente
La inyección de dependencia es, como la mayoría de los patrones, una solución a los problemas . Comience preguntando si incluso tiene el problema en primer lugar. Si no es así, a continuación, utilizando el patrón lo más probable es que el código sea peor .
Considere primero si puede reducir o eliminar dependencias. En igualdad de condiciones, queremos que cada componente de un sistema tenga la menor cantidad de dependencias posible. Y si las dependencias desaparecen, la cuestión de inyectar o no se vuelve discutible.
Considere un módulo que descarga algunos datos de un servicio externo, los analiza, realiza algunos análisis complejos y escribe los resultados en un archivo.
Ahora, si la dependencia del servicio externo está codificada, será realmente difícil probar el procesamiento interno de este módulo. Por lo tanto, puede decidir inyectar el servicio externo y el sistema de archivos como dependencias de la interfaz, lo que le permitirá inyectar simulacros en su lugar, lo que a su vez hace posible la prueba interna de la lógica interna.
Pero una solución mucho mejor es simplemente separar el análisis de la entrada / salida. Si el análisis se extrae a un módulo sin efectos secundarios, será mucho más fácil probarlo. Tenga en cuenta que la burla es un olor a código: no siempre es evitable, pero en general, es mejor si puede probar sin depender de la burla. Entonces, al eliminar las dependencias, se evitan los problemas que DI debe aliviar. Tenga en cuenta que dicho diseño también se adhiere mucho mejor a SRP.
Quiero enfatizar que DI no necesariamente facilita SRP u otros buenos principios de diseño como separación de preocupaciones, alta cohesión / bajo acoplamiento, etc. También podría tener el efecto contrario. Considere una clase A que usa otra clase B internamente. B solo es utilizado por A y, por lo tanto, está completamente encapsulado y puede considerarse un detalle de implementación. Si cambia esto para inyectar B en el constructor de A, entonces ha expuesto estos detalles de implementación y ahora sabe sobre esta dependencia y sobre cómo inicializar B, la vida útil de B, etc., debe existir en otro lugar del sistema por separado de A. Entonces, tiene una arquitectura en general peor con problemas de fugas.
Por otro lado, hay algunos casos en los que la DI realmente es útil. Por ejemplo, para servicios globales con efectos secundarios como un registrador.
El problema es cuando los patrones y las arquitecturas se convierten en objetivos en sí mismos en lugar de herramientas. Solo preguntaba "¿Deberíamos usar DI?" es como poner el carro delante del caballo. Debe preguntar: "¿Tenemos un problema?" y "¿Cuál es la mejor solución para este problema?"
Una parte de su pregunta se reduce a: "¿Deberíamos crear interfaces superfluas para satisfacer las demandas del patrón?" Probablemente ya se haya dado cuenta de la respuesta a esto, ¡ absolutamente no ! Cualquiera que le diga lo contrario está tratando de venderle algo, muy probablemente costosas horas de consultoría. Una interfaz solo tiene valor si representa una abstracción. Una interfaz que simplemente imita la superficie de una sola clase se denomina "interfaz de encabezado" y es un antipatrón conocido.
fuente
A
usaB
en producción, pero solo se ha probadoMockB
, nuestras pruebas no nos dicen si funcionará en producción. Cuando los componentes puros (sin efectos secundarios) de un modelo de dominio se inyectan y se burlan entre sí, el resultado es una gran pérdida de tiempo para todos, una base de código hinchada y frágil, y poca confianza en el sistema resultante. Burlarse de los límites del sistema, no entre piezas arbitrarias del mismo sistema.En mi experiencia, hay varias desventajas en la inyección de dependencia.
Primero, el uso de DI no simplifica las pruebas automatizadas tanto como se anuncia. La unidad que prueba una clase con una implementación simulada de una interfaz le permite validar cómo interactuará esa clase con la interfaz. Es decir, le permite a la unidad probar cómo la clase bajo prueba utiliza el contrato provisto por la interfaz. Sin embargo, esto proporciona una seguridad mucho mayor de que la entrada de la clase bajo prueba en la interfaz es la esperada. Proporciona una garantía bastante pobre de que la clase bajo prueba responde como se espera que salga de la interfaz, ya que es una salida simulada casi universalmente, que está sujeta a errores, simplificaciones excesivas, etc. En resumen, NO le permite validar que la clase se comportará como se espera con una implementación real de la interfaz.
En segundo lugar, DI hace que sea mucho más difícil navegar a través del código. Al intentar navegar a la definición de clases utilizadas como entrada para funciones, una interfaz puede ser desde una molestia menor (por ejemplo, donde hay una implementación única) hasta un sumidero de tiempo mayor (por ejemplo, cuando se usa una interfaz demasiado genérica como IDisposable) al intentar encontrar la implementación real que se está utilizando. Esto puede convertir un ejercicio simple como "Necesito corregir una excepción de referencia nula en el código que sucede justo después de que se imprima esta declaración de registro" en un esfuerzo de un día.
Tercero, el uso de DI y marcos es una espada de doble filo. Puede reducir en gran medida la cantidad de código de placa de caldera necesaria para operaciones comunes. Sin embargo, esto se produce a expensas de necesitar un conocimiento detallado del marco DI particular para comprender cómo estas operaciones comunes están realmente conectadas entre sí. Comprender cómo se cargan las dependencias en el marco y agregar una nueva dependencia en el marco para inyectar puede requerir leer una buena cantidad de material de base y seguir algunos tutoriales básicos sobre el marco. Esto puede convertir algunas tareas simples en tareas que requieren mucho tiempo.
fuente
Seguí el consejo de Mark Seemann de "Inyección de dependencias en .NET" - para suponer.
DI debe usarse cuando tiene una 'dependencia volátil', por ejemplo, existe una posibilidad razonable de que pueda cambiar.
Entonces, si cree que podría tener más de una implementación en el futuro o la implementación podría cambiar, use DI. De
new
lo contrario está bien.fuente
Mi mayor motivo favorito sobre la DI ya se mencionó en algunas respuestas de manera pasajera, pero voy a expandirlo un poco aquí. DI (como se hace principalmente hoy en día, con contenedores, etc.) realmente, REALMENTE daña la legibilidad del código. Y la legibilidad del código es posiblemente la razón detrás de la mayoría de las innovaciones de programación actuales. Como alguien dijo, escribir código es fácil. Leer el código es difícil. Pero también es extremadamente importante, a menos que esté escribiendo algún tipo de pequeña utilidad desechable de escritura única.
El problema con DI a este respecto es que es opaco. El contenedor es una caja negra. Los objetos simplemente aparecen en algún lugar y no tienes idea: ¿quién los construyó y cuándo? ¿Qué le pasó al constructor? ¿Con quién estoy compartiendo esta instancia? Quién sabe...
Y cuando trabajas principalmente con interfaces, todas las funciones de "ir a la definición" de tu IDE desaparecen. Es terriblemente difícil rastrear el flujo del programa sin ejecutarlo y simplemente pasar para ver QUÉ implementación de la interfaz se usó en ESTE lugar en particular. Y de vez en cuando hay algún obstáculo técnico que le impide avanzar. E incluso si puede, si implica pasar por los intestinos retorcidos del contenedor DI, todo el asunto rápidamente se convierte en un ejercicio de frustración.
Para trabajar de manera eficiente con un código que utiliza DI, debe estar familiarizado con él y saber qué va a dónde.
fuente
Si bien la DI en general es seguramente algo bueno, sugeriría no usarla a ciegas para todo. Por ejemplo, nunca inyecto registradores. Una de las ventajas de DI es hacer que las dependencias sean explícitas y claras. No tiene sentido enumerar
ILogger
como dependencia de casi todas las clases, es simplemente desorden. Es responsabilidad del registrador proporcionar la flexibilidad que necesita. Todos mis registradores son miembros finales estáticos, puedo considerar inyectar un registrador cuando necesito uno no estático.Esta es una desventaja del marco DI o marco burlón dado, no del DI en sí. En la mayoría de los lugares, mis clases dependen de clases concretas, lo que significa que no se necesita cero repetitivo. Guice (un marco Java DI) enlaza por defecto una clase a sí mismo y solo necesito anular el enlace en las pruebas (o conectarlas manualmente en su lugar).
Solo creo las interfaces cuando es necesario (lo cual es bastante raro). Esto significa que a veces, tengo que reemplazar todas las ocurrencias de una clase por una interfaz, pero el IDE puede hacer esto por mí.
Sí, pero evite cualquier repetitivo adicional .
No. Habrá muchas clases de valor (inmutable) y de datos (mutable), donde las instancias simplemente se crean y pasan y donde no tiene sentido inyectarlas, ya que nunca se almacenan en otro objeto (o solo en otro tales objetos).
Para ellos, es posible que deba inyectar una fábrica, pero la mayoría de las veces no tiene sentido (imagine, por ejemplo,
@Value class NamedUrl {private final String name; private final URL url;}
que realmente no necesita una fábrica aquí y no hay nada que inyectar).En mi humilde opinión, está bien, siempre y cuando no causa la hinchazón de código. Inyecte la dependencia, pero no cree la interfaz (¡y sin XML de configuración loca!), Ya que puede hacerlo más tarde sin problemas.
En realidad, en mi proyecto actual, hay cuatro clases (de cientos), que decidí excluir de DI ya que son clases simples utilizadas en demasiados lugares, incluidos los objetos de datos.
Otra desventaja de la mayoría de los marcos DI es la sobrecarga de tiempo de ejecución. Esto se puede mover al tiempo de compilación (para Java, hay Dagger , no tengo idea de otros idiomas).
Otra desventaja es la magia que ocurre en todas partes, que se puede desactivar (por ejemplo, deshabilité la creación de proxy cuando uso Guice).
fuente
Debo decir que, en mi opinión, toda la noción de inyección de dependencia está sobrevalorada.
DI es el equivalente moderno de los valores globales. Las cosas que está inyectando son singletons globales y objetos de código puro, de lo contrario, no podría inyectarlos. La mayoría de los usos de DI se ven obligados a utilizar una biblioteca determinada (JPA, Spring Data, etc.). En su mayor parte, DI proporciona el ambiente perfecto para nutrir y cultivar espaguetis.
Honestamente, la forma más fácil de probar una clase es asegurarse de que todas las dependencias se creen en un método que se pueda anular. Luego cree una clase de prueba derivada de la clase real y anule ese método.
Luego crea una instancia de la clase Test y prueba todos sus métodos. Esto no será claro para algunos de ustedes: los métodos que están probando son los que pertenecen a la clase bajo prueba. Y todas estas pruebas de método ocurren en un archivo de clase única: la clase de prueba unitaria asociada con la clase bajo prueba. Aquí hay cero gastos generales: así es como funcionan las pruebas unitarias.
En código, este concepto se ve así ...
El resultado es exactamente equivalente al uso de DI, es decir, el ClassUnderTest está configurado para pruebas.
Las únicas diferencias son que este código es completamente conciso, completamente encapsulado, más fácil de codificar, más fácil de entender, más rápido, usa menos memoria, no requiere una configuración alternativa, no requiere marcos, nunca será la causa de una página 4 (¡WTF!) Seguimiento de pila que incluye exactamente CERO (0) clases que usted escribió, y es completamente obvio para cualquier persona con el más mínimo conocimiento de OO, desde principiante hasta Guru (pensaría, pero estaría equivocado).
Dicho esto, por supuesto que no podemos usarlo: es demasiado obvio y no está lo suficientemente a la moda.
Al final del día, sin embargo, mi mayor preocupación con DI es la de los proyectos que he visto fallar miserablemente, todos ellos han sido bases de código masivas donde DI era el pegamento que mantenía todo junto. DI no es una arquitectura: en realidad solo es relevante en un puñado de situaciones, la mayoría de las cuales se le imponen para usar otra biblioteca (JPA, Spring Data, etc.). En su mayor parte, en una base de código bien diseñada, la mayoría de los usos de DI se producirían en un nivel por debajo de donde tienen lugar sus actividades de desarrollo diarias.
fuente
bar
solo se puede llamar despuésfoo
, entonces esa es una API incorrecta. Está reclamando para proporcionar funcionalidad (bar
), pero que en realidad no puede usarlo (ya quefoo
podrían no haber sido llamado). Si desea seguir con suinitializeDependencies
patrón (anti?), Al menos debe hacerlo privado / protegido y llamarlo automáticamente desde el constructor, por lo que la API es sincera.Realmente su pregunta se reduce a "¿La unidad está mal?
El 99% de sus clases alternativas para inyectar serán simulacros que permitan la prueba de la unidad.
Si realiza pruebas unitarias sin DI, tiene el problema de cómo hacer que las clases usen datos simulados o un servicio simulado. Digamos 'parte de la lógica', ya que es posible que no lo esté separando en servicios.
Hay formas alternativas de hacerlo, pero la DI es buena y flexible. Una vez que lo tiene para probar, se ve prácticamente obligado a usarlo en todas partes, ya que necesita otra pieza de código, incluso el llamado 'DI del pobre' que crea instancias de los tipos concretos.
Es difícil imaginar una desventaja tan grave que la ventaja de las pruebas unitarias se vea desbordada.
fuente
new
dentro de un método, sabemos que el mismo código que se ejecuta en producción se ejecutaba durante las pruebas.Order
ejemplo es una prueba unitaria: está probando un método (eltotal
método deOrder
). Te estás quejando de que está llamando a un código de 2 clases, mi respuesta es ¿y qué ? No estamos probando "2 clases a la vez", estamos probando eltotal
método. No debería importarnos cómo un método hace su trabajo: esos son detalles de implementación; probarlos provoca fragilidad, acoplamiento estrecho, etc. Solo nos preocupa el comportamiento del método (valor de retorno y efectos secundarios), no las clases / módulos / registros de CPU / etc. Se utiliza en el proceso.