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

109

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:

  • Posibilitar / facilitar el aislamiento en la prueba unitaria
  • Definir explícitamente las dependencias de una clase
  • Facilitando un buen diseño ( principio de responsabilidad única (SRP) por ejemplo)
  • Habilitando las implementaciones de conmutación rápidamente (por ejemplo, DbLogger en lugar de ConsoleLogger )

Reconozco que existe un consenso general en la industria de que DI es un patrón bueno y útil. No hay demasiadas críticas por el momento. Las desventajas que se mencionan en la comunidad son generalmente menores. Algunos de ellos:

  • Mayor número de clases
  • Creación de interfaces innecesarias

Actualmente hablamos de diseño de arquitectura con mi colega. Es bastante conservador, pero de mentalidad abierta. A él le gusta cuestionar cosas, lo que considero bueno, porque muchas personas en TI simplemente copian la nueva tendencia, repiten las ventajas y, en general, no piensan demasiado, no analicen 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 idea es mala (supongamos que solo tenemos una implementación, por lo que no queremos crear una interfaz "vacía") si no planeamos probar una clase en particular?
pregunta Landeeyo 29.05.2018 - 13:20
fuente

8 respuestas

152

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 principal proporciona todas las dependencias necesarias para el objeto secundario.

Eso es todo. Tenga en cuenta que nada requiere interfaces, marcos, ningún estilo de inyección, etc. Para ser justos, aprendí 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 una instancia y configura el objeto hijo que usa
  • El hijo es el componente que está diseñado para crear una instancia pasiva. Es decir. está diseñado para usar las dependencias proporcionadas por el padre y no crea instancias propias.

La inyección de dependencia es un patrón para el objeto composición .

¿Por qué interfaces?

Las interfaces son un contrato. Existen para limitar cuán estrechamente acoplados pueden ser dos objetos. No todas las dependencias necesitan una interfaz, pero ayudan a escribir código modular.

Cuando agrega el concepto de prueba unitaria, puede tener dos implementaciones conceptuales para cualquier interfaz dada: el objeto real que desea usar en su aplicación y el objeto simulado o aplastado que usa para probar el código que depende de la objeto. Solo eso 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 un gran número de ellos. Los marcos proporcionan los siguientes beneficios:

  • Dependencia automática de dependencias a componentes
  • Configuración de los componentes con ajustes de algún tipo
  • Automatice 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 principal 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 mediante 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 (la programación orientada a aspectos incorporada a Spring viene a la mente). El contenedor es una caja negra, y no siempre se construyen con ningún concepto de facilitar el proceso de depuración.

Todo lo dicho, 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 hay ciertos componentes ya creados para usted, el marco se puede justificar.

¿Qué pasa con [artículo aleatorio en Internet]?

¿Qué pasa con eso? Muchas veces las personas pueden ponerse demasiado celosas y agregar un montón de restricciones y reprenderte si no estás haciendo las cosas de la "única manera verdadera". No hay un camino verdadero. 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.

Trabajar con "viejos jefes"

Aprende todo lo que puedas. Lo que encontrarás con muchos desarrolladores que están trabajando en sus años 70 es que han aprendido a no ser dogmáticos con 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 una retroalimentación brutalmente honesta que tiene mucho sentido. Y donde ven valor, agregan esas herramientas a su repertorio.

    
respondido por el Berin Loritsch 29.05.2018 - 15:03
fuente
79

La inyección de dependencia es, como la mayoría de los patrones, una solución a los problemas . Así que empieza preguntando si tienes el problema en primer lugar. De lo contrario, usar el patrón probablemente empeorará el código peor .

Considere primero si puede reducir o eliminar las dependencias. En igualdad de condiciones, queremos que cada componente de un sistema tenga la menor cantidad de dependencias posible. ¡Y si las dependencias se han ido, 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 un análisis complejo y escribe en un archivo como resultado.

Ahora, si la dependencia del servicio externo está codificada, entonces 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, lo que a su vez hace que la prueba de la lógica interna sea posible.

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 de probar. Tenga en cuenta que la burla es un olor a código; no siempre es evitable, pero en general, es mejor si puede hacer pruebas sin confiar en la burla. Entonces al eliminar las dependencias, evita los problemas que DI debe aliviar. Tenga en cuenta que dicho diseño también se adhiere mucho mejor a SRP.

Quiero enfatizar que el DI no facilita necesariamente el SRP u otros principios de buen diseño, como la separación de inquietudes, 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 este detalle de implementación y ahora el conocimiento sobre esta dependencia y sobre cómo inicializar B, la vida útil de B, etc., debe existir en algún otro lugar en el sistema por separado. de A. Así que tienes una arquitectura peor en general con problemas de fugas.

Por otro lado, hay algunos casos en los que DI es realmente ú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 preguntando "¿Debemos usar DI?" Es como poner el carro delante del caballo. Deberías 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 esta pregunta: absolutamente no . Cualquier persona que le diga lo contrario está intentando venderle algo, lo más probable es que sean horas de consulta caras. Una interfaz solo tiene valor si representa una abstracción. Una interfaz que simplemente imita la superficie de una sola clase se llama "interfaz de encabezado" y esto es un antipattern conocido.

    
respondido por el JacquesB 29.05.2018 - 15:34
fuente
33

En mi experiencia, hay una serie de desventajas en la inyección de dependencia.

Primero, el uso de DI no simplifica las pruebas automatizadas tanto como lo anuncian. La prueba unitaria de una clase con una implementación simulada de una interfaz le permite validar cómo esa clase interactuará con la interfaz. Es decir, le permite realizar una prueba unitaria de cómo la clase bajo prueba usa 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 casi universalmente simulada, que a su vez 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 por el código. Cuando se trata de navegar a la definición de las clases utilizadas como entrada a las funciones, una interfaz puede ser desde una pequeña molestia (por ejemplo, cuando hay una implementación única) hasta un sumidero de tiempo importante (por ejemplo, cuando se utiliza una interfaz demasiado genérica como IDisposable) cuando se trata de encontrar la implementación real que se está utilizando. Esto puede convertir un ejercicio simple como "Necesito arreglar una excepción de referencia nula en el código que ocurre justo después de que se imprime 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 las operaciones comunes. Sin embargo, esto conlleva el hecho de necesitar un conocimiento detallado del marco DI particular para comprender cómo estas operaciones comunes están conectadas entre sí. Comprender cómo se cargan las dependencias en el marco y agregar una nueva dependencia al marco para inyectar puede requerir leer una buena cantidad de material de fondo y seguir algunos tutoriales básicos sobre el marco. Esto puede convertir algunas tareas simples en tareas que consumen bastante tiempo.

    
respondido por el Eric 29.05.2018 - 23:44
fuente
12

Seguí el consejo de Mark Seemann de "Inyección de dependencia en .NET" - para suponer.

Debería usarse DI cuando tienes una 'dependencia volátil', por ejemplo. hay una posibilidad razonable de que pueda cambiar.

Entonces, si piensa que podría tener más de una implementación en el futuro o la implementación podría cambiar, use DI. De lo contrario, new está bien.

    
respondido por el Rob 29.05.2018 - 13:38
fuente
2
  

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

Aunque DI en general es una buena cosa, sugeriría no usarlo a ciegas para todo. Por ejemplo, nunca me inyecto madereros. 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 un desorden. Es responsabilidad del registrador proporcionar la flexibilidad que necesita. Todos mis registradores son miembros finales estáticos, puedo considerar la posibilidad de inyectar un registrador cuando necesito uno no estático.

  

Mayor número de clases

Esto es una desventaja del marco DI o del 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) vincula de forma predeterminada una clase a sí mismo y solo necesito anular el enlace en las pruebas (o cablearlas manualmente).

  

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 apariciones 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 evita las repeticiones adicionales .

  

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

No. Habrá muchas clases de valor (inmutable) y de datos (mutables), donde las instancias simplemente se crean y se transmiten y no tienen sentido al inyectarlos, ya que nunca se almacenan en otro objeto (o solo en otros objetos similares).

Para ellos, es posible que necesites inyectar una fábrica, pero la mayoría de las veces no tiene sentido (imagina, por ejemplo, @Value class NamedUrl {private final String name; private final URL url;} ; realmente no necesitas una fábrica aquí y no hay nada que inyectar).

  

¿Inyectar una sola idea es mala (supongamos que solo tenemos una implementación, por lo que no queremos crear una interfaz "vacía") si no planeamos realizar una prueba unitaria de una clase en particular?

En mi humilde opinión está bien, siempre y cuando no cause un aumento de código. Inyecte la dependencia pero no cree la interfaz (¡y ninguna configuración XML loca!) Como puede hacerlo más tarde sin ningún problema.

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 objetos de datos.

Otra desventaja de la mayoría de los marcos DI es la sobrecarga del tiempo de ejecución. Esto se puede mover al tiempo de compilación (para Java, hay Dagger , ni idea sobre otros idiomas).

Otra desventaja es que la magia está sucediendo en todas partes, lo que se puede reducir (por ejemplo, deshabilité la creación de proxy cuando uso Guice).

    
respondido por el maaartinus 01.06.2018 - 11:16
fuente
2

Mi mayor queja sobre la DI ya se mencionó en algunas respuestas de manera pasajera, pero lo ampliaré un poco aquí. DI (como se hace en su mayoría hoy, 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 de hoy. Como dijo alguien, 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 de escritura de un solo uso.

El problema con DI en este aspecto es que es opaco. El contenedor es una caja negra. Los objetos simplemente aparecen desde algún lugar y no tienes idea, ¿quién los construyó y cuándo? ¿Qué fue pasado al constructor? ¿Con quién estoy compartiendo esta instancia? Quien sabe ...

Y cuando trabajas principalmente con interfaces, todas las características de "ir a la definición" de tu IDE se vuelven humo. Es terriblemente difícil rastrear el flujo del programa sin ejecutarlo y solo pasar para ver exactamente QUÉ implementación del interfaz se usó en ESTE lugar en particular. Y de vez en cuando hay algún obstáculo técnico que le impide pasar. E incluso si puedes, si implica atravesar las entrañas retorcidas del contenedor DI, todo el asunto se convierte rápidamente en un ejercicio de frustración.

Para trabajar de manera eficiente con un fragmento de código que usó DI, debe estar familiarizado con él y ya saber qué va a dónde.

    
respondido por el Vilx- 03.06.2018 - 21:15
fuente
-5

Tengo que decir que, en mi opinión, toda la noción de inyección de dependencia está sobrevaluada.

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 son forzados para usar 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 pueda ser anulado. Luego cree una clase de prueba derivada de la clase real y anule ese método.

Luego, crea una instancia de la clase Prueba y prueba todos sus métodos. Esto no será claro para algunos de ustedes: los métodos que está probando son los que pertenecen a la clase bajo prueba. Y todas estas pruebas de métodos se realizan en un solo archivo de clase: la clase de prueba unitaria asociada con la clase bajo prueba. Aquí no hay sobrecarga, así es como funcionan las pruebas unitarias.

En el 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 la prueba.

Las únicas diferencias son que este código es completamente conciso, completamente encapsulado, 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 ningún marco, nunca será la causa de un seguimiento de pila de 4 páginas (WTF!) que incluye exactamente CERO (0) clases que usted escribió, y es completamente obvio para cualquiera 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 fracasar miserablemente, todos ellos han sido bases de código masivas donde DI era el pegamento que mantenía todo unido. DI no es una arquitectura, en realidad solo es relevante en un puñado de situaciones, la mayoría de las cuales son forzadas 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 del lugar donde se realizan sus actividades de desarrollo diarias.

    
respondido por el Rodney P. Barbati 30.05.2018 - 05:43
fuente
-6

Realmente su pregunta se reduce a "¿Son malas las pruebas de unidad?"

El 99% de tus clases alternativas para inyectar serán simulacros que habilitan las pruebas unitarias.

Si realiza pruebas de unidad 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 hacer esto, pero DI es una buena y flexible. Una vez que lo tiene para probar, prácticamente se ve obligado a usarlo en todas partes, ya que necesita algún otro código, incluso el llamado 'DI de hombre pobre' que ejemplifica los tipos concretos.

Es difícil imaginar una desventaja tan grave que la ventaja de las pruebas unitarias se vea superada.

    
respondido por el Ewan 29.05.2018 - 13:36
fuente

Lea otras preguntas en las etiquetas