Críticas y desventajas de la inyección de dependencia

118

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 DbLoggerlugar de, ConsoleLoggerpor 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?
Landeeyo
fuente
33
¿Realmente está preguntando acerca de la inyección de dependencia como patrón, o está preguntando sobre el uso de marcos DI? Estas son cosas realmente distintas, debe aclarar qué parte del problema le interesa o preguntar explícitamente sobre ambas.
Frax
10
@Frax sobre patrón, no marcos
Landeeyo
10
Estás confundiendo la inversión de dependencia con la inyección de dependencia. El primero es un principio de diseño. Esta última es una técnica (generalmente implementada con una herramienta existente) para construir jerarquías de objetos.
jpmc26
3
A menudo escribo pruebas usando la base de datos real y sin objetos simulados. Funciona muy bien en muchos casos. Y luego no necesita interfaces la mayor parte del tiempo. Si tiene una UserServiceclase, 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.
usr
3
La DI casi siempre es buena. Lo malo de esto es que muchas personas piensan que saben DI pero todo lo que saben es cómo usar un marco extraño sin siquiera estar seguros de lo que están haciendo. DI hoy en día sufre mucho de la programación de culto de carga.
T. Sar

Respuestas:

160

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:

Un objeto primario proporciona todas las dependencias requeridas para el objeto secundario.

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:

  • El padre es el objeto que crea instancias y configura el objeto hijo que usa
  • El niño es el componente que está diseñado para ser instanciado pasivamente. Es decir, está diseñado para usar las dependencias proporcionadas por el padre, y no crea sus propias dependencias.

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:

  • Autoconexión de dependencias a componentes
  • Configurar los componentes con ajustes de algún tipo
  • Automatizar el código de la placa de la caldera para que no tenga que verlo escrito en varias ubicaciones.

También tienen las siguientes desventajas:

  • El objeto padre es un "contenedor", y no hay nada en su código.
  • Hace que las pruebas sean más complicadas si no puede proporcionar las dependencias directamente en su código de prueba
  • Puede ralentizar la inicialización ya que resuelve todas las dependencias usando la reflexión y muchos otros trucos.
  • La depuración en tiempo de ejecución puede ser más difícil, especialmente si el contenedor inyecta un proxy entre la interfaz y el componente real que implementa la interfaz (viene a la mente la programación orientada a aspectos integrada en Spring). El contenedor es una caja negra, y no siempre se construyen con el concepto de facilitar el proceso de depuración.

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.

Berin Loritsch
fuente
66
@CarlLeth, he trabajado con una serie de frameworks desde Spring hasta variantes de .net. Spring le permitirá inyectar implementaciones directamente en campos privados utilizando algo de magia negra de reflexión / carga de clases. La única forma de probar componentes construidos de esa manera es usar el contenedor. Spring tiene corredores JUnit para configurar el entorno de prueba, pero es más complicado que configurarlo usted mismo. Entonces sí, acabo de dar un ejemplo práctico .
Berin Loritsch
17
Hay una desventaja más que encuentro que es un obstáculo con DI a través de los marcos cuando llevo mi sombrero de solucionador de problemas / mantenedor: la acción espeluznante a la distancia que proporcionan hace que la depuración fuera de línea sea más difícil. En el peor de los casos, tengo que ejecutar el código para ver cómo se inicializan y pasan las dependencias. Mencionas esto en el contexto de "pruebas", pero en realidad es mucho peor si comienzas a mirar la fuente, nunca importa intentar ejecutarlo (lo que puede implicar una gran cantidad de configuración). Impedir mi capacidad de saber qué hace el código con solo mirarlo es algo malo.
Jeroen Mostert
1
Las interfaces no son contratos, son simplemente API. Los contratos implican semántica. Esta respuesta está utilizando terminología específica del lenguaje y convenciones específicas de Java / C #.
Frank Hileman
2
@BerinLoritsch El punto principal de su propia respuesta es que el principio DI! = Cualquier marco DI dado. El hecho de que Spring pueda hacer cosas horribles e imperdonables es una desventaja de Spring, no de los marcos DI en general. Un buen marco DI le ayuda a seguir el principio DI sin trucos desagradables.
Carl Leth
1
@CarlLeth: todos los marcos DI están diseñados para eliminar o automatizar algunas cosas que el programador no desea explicar, solo varían en cómo. Que yo sepa, todos eliminan la capacidad de saber cómo (o si ) las clases A y B interactúan con solo mirar A y B; al menos, también debes mirar la configuración / configuración de DI / convenciones No es un problema para el programador (es exactamente lo que quieren, en realidad), sino un problema potencial para un mantenedor / depurador (posiblemente el mismo programador, más adelante). Esta es una compensación que realiza incluso si su marco DI es "perfecto".
Jeroen Mostert
88

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.

JacquesB
fuente
15
¡No podría estar mas de acuerdo! También tenga en cuenta que burlarse de cosas significa que en realidad no estamos probando las implementaciones reales. Si se Ausa Ben producción, pero solo se ha probado MockB, 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.
Warbo
17
@CarlLeth ¿Por qué crees que DI hace que el código sea "comprobable y mantenible" y que el código sea menos? JacquesB tiene razón en que los efectos secundarios son lo que perjudica la capacidad de prueba / mantenibilidad. Si el código no tiene efectos secundarios, no nos importa qué / dónde / cuándo / cómo llama a otro código; podemos mantenerlo simple y directo. Si el código tiene efectos colaterales que tienen a la atención. DI puede extraer efectos secundarios de las funciones y ponerlo en parámetros, haciendo que esas funciones sean más comprobables pero el programa más complejo. A veces eso es inevitable (por ejemplo, acceso a la base de datos). Si el código no tiene efectos secundarios, la DI es simplemente una complejidad inútil.
Warbo
13
@CarlLeth: DI es una solución al problema de hacer que el código sea comprobable de forma aislada si tiene dependencias que lo prohíben. Pero no reduce la complejidad general, ni hace que el código sea más legible, lo que significa que no necesariamente aumenta la capacidad de mantenimiento. Sin embargo, si todas esas dependencias pueden eliminarse mediante una mejor separación de las preocupaciones, esto "anula" perfectamente los beneficios de la DI, porque anula la necesidad de DI. Esta suele ser una mejor solución para hacer que el código sea más comprobable y mantenible al mismo tiempo.
Doc Brown
55
@Warbo Este fue el original y probablemente el único uso válido de la burla. Incluso en los límites del sistema, rara vez se necesita. La gente realmente pierde mucho tiempo creando y actualizando pruebas casi sin valor.
Frank Hileman
66
@CarlLeth: ok, ahora veo de dónde viene el malentendido. Estás hablando de inversión de dependencia . Pero, la pregunta, esta respuesta y mis comentarios son sobre DI = inyección de dependencia .
Doc Brown
36

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.

Eric
fuente
También agregaría que cuanto más se inyecte, más largos serán los tiempos de inicio. La mayoría de los marcos DI crean todas las instancias de inyección única en el momento del inicio, independientemente de dónde se usen.
Rodney P. Barbati
77
Si desea probar una clase con implementación real (no simulada), puede escribir pruebas funcionales, pruebas similares a las pruebas unitarias, pero que no utilizan simulacros.
B 30овић
2
Creo que su segundo párrafo debe ser más matizado: DI en sí mismo no dificulta (er) navegar a través del código. En su forma más simple, DI es simplemente una consecuencia de seguir a SOLID. Lo que aumenta la complejidad es el uso de marcos indirectos y DI innecesarios . Aparte de eso, esta respuesta da en el clavo.
Konrad Rudolph
44
La inyección de dependencia, fuera de los casos en los que realmente se necesita, también es una señal de advertencia de que otro código superfluo puede estar presente en gran abundancia. Los ejecutivos a menudo se sorprenden al saber que los desarrolladores agregan complejidad por el bien de la complejidad.
Frank Hileman
1
+1. Esta es la respuesta real a la pregunta que se hizo, y debe ser la respuesta aceptada.
Mason Wheeler
13

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 newlo contrario está bien.

Robar
fuente
55
Tenga en cuenta que también da diferentes consejos para OO y lenguajes funcionales blog.ploeh.dk/2017/01/27/…
jk.
1
Ese es un buen punto. Si creamos interfaces para cada dependencia de forma predeterminada, entonces de alguna manera es contra YAGNI.
Landeeyo
44
¿Puede proporcionar una referencia a "Inyección de dependencias en .NET" ?
Peter Mortensen
1
Si realiza pruebas unitarias, es muy probable que su dependencia sea volátil.
Jaquez
11
Lo bueno es que los desarrolladores siempre pueden predecir el futuro con una precisión perfecta.
Frank Hileman
5

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.

Vilx-
fuente
3

Habilitar las implementaciones de conmutación rápidamente (DbLogger en lugar de ConsoleLogger, por ejemplo)

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 ILoggercomo 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.

Mayor número de clases.

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).

Creación de interfaces innecesarias.

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í.

¿Deberíamos usar la inyección de dependencia cuando solo tenemos una implementación?

Sí, pero evite cualquier repetitivo adicional .

¿Deberíamos prohibir la creación de nuevos objetos, excepto los de lenguaje / marco?

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).

¿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?

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).

maaartinus
fuente
-4

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í ...

class ClassUnderTest {

   protected final ADependency;
   protected final AnotherDependency;

   // Call from a factory or use an initializer 
   public void initializeDependencies() {
      aDependency = new ADependency();
      anotherDependency = new AnotherDependency();
   }
}

class TestClassUnderTest extends ClassUnderTest {

    @Override
    public void initializeDependencies() {
      aDependency = new MockitoObject();
      anotherDependency = new MockitoObject();
    }

    // Unit tests go here...
    // Unit tests call base class methods
}

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.

Rodney P. Barbati
fuente
66
No ha descrito el equivalente, sino lo contrario de la inyección de dependencia. En su modelo, cada objeto necesita conocer la implementación concreta de todas sus dependencias, pero el uso de DI que se convierte en responsabilidad del componente "principal" es unir las implementaciones apropiadas. Tenga en cuenta que la DI va de la mano con la otra DI: inversión de dependencia, donde no desea que los componentes de alto nivel tengan dependencias duras de los componentes de bajo nivel.
Recogida de Logan
1
es ordenado cuando solo tiene un nivel de herencia de clase y un nivel de dependencias. ¿Seguramente se convertirá en un infierno en la tierra a medida que se expanda?
Ewan
44
si usa interfaces y mueve initializeDependencies () al constructor, es igual pero más ordenado. El siguiente paso, agregar parámetros de construcción significa que puede eliminar todas sus clases de prueba.
Ewan
55
Hay mucho de malo en esto. Como otros han dicho, su ejemplo de 'DI equivalente' no es la inyección de dependencia en absoluto, es la antítesis, y demuestra una completa falta de comprensión del concepto e introduce otras trampas potenciales: los objetos parcialmente inicializados son un olor a código Como Ewan sugiere mover la inicialización al constructor y pasarlos a través de los parámetros del constructor. Entonces tienes DI ...
Mr.Mindor
3
Agregando a @ Mr.Mindor: hay un "acoplamiento secuencial" antipatrón más general que no solo se aplica a la inicialización. Si los métodos de un objeto (o, más generalmente, las llamadas de una API) deben ejecutarse en un orden particular, por ejemplo, barsolo se puede llamar después foo, entonces esa es una API incorrecta. Está reclamando para proporcionar funcionalidad ( bar), pero que en realidad no puede usarlo (ya que foopodrían no haber sido llamado). Si desea seguir con su initializeDependenciespatrón (anti?), Al menos debe hacerlo privado / protegido y llamarlo automáticamente desde el constructor, por lo que la API es sincera.
Warbo
-6

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.

Ewan
fuente
13
No estoy de acuerdo con su afirmación de que DI se trata de pruebas unitarias. Facilitar las pruebas unitarias es solo uno de los beneficios de la DI, y posiblemente no sea el más importante.
Robert Harvey
55
No estoy de acuerdo con su premisa de que las pruebas unitarias y la DI están tan cerca. Al usar un simulacro / código auxiliar, hacemos que el conjunto de pruebas nos mienta un poco más: el sistema bajo prueba se aleja del sistema real. Eso es objetivamente malo. A veces eso es mayor que una ventaja: las llamadas FS burladas no requieren limpieza; las solicitudes HTTP simuladas son rápidas, deterministas y funcionan sin conexión; Por el contrario, cada vez que utilizamos un código codificado newdentro de un método, sabemos que el mismo código que se ejecuta en producción se ejecutaba durante las pruebas.
Warbo
8
No, no es "¿la unidad está probando mal?", Es "¿es burlón (a) realmente necesario y (b) vale la pena el aumento de la complejidad incurrida?" Esa es una pregunta muy diferente. Las pruebas unitarias no son malas (literalmente, nadie discute esto), y definitivamente vale la pena en general. Pero no todas las pruebas unitarias requieren burla, y la burla conlleva un costo sustancial, por lo que, al menos, debe usarse con prudencia.
Konrad Rudolph
66
@Ewan Después de tu último comentario, no creo que estemos de acuerdo. Estoy diciendo que la mayoría de las pruebas unitarias no necesitan DI [marcos] , porque la mayoría de las pruebas unitarias no necesitan burlarse. De hecho, incluso usaría esto como una heurística para la calidad del código: si la mayor parte de su código no puede probarse sin unidades DI / objetos simulados, entonces ha escrito un código incorrecto que se acopló demasiado correctamente. La mayoría de los códigos deben estar altamente desacoplados, ser de responsabilidad única y de uso general, y ser trivialmente verificables de forma aislada.
Konrad Rudolph
55
@Ewan Su enlace ofrece una buena definición de pruebas unitarias. Según esa definición, mi Orderejemplo es una prueba unitaria: está probando un método (el totalmétodo de Order). 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 el totalmé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.
Warbo