¿Cuándo no es apropiado usar el patrón de inyección de dependencia?

112

Desde que aprendí (y me encantaron) las pruebas automatizadas, me he encontrado usando el patrón de inyección de dependencia en casi todos los proyectos. ¿Siempre es apropiado usar este patrón cuando se trabaja con pruebas automatizadas? ¿Hay alguna situación en la que deba evitar el uso de la inyección de dependencia?

    
pregunta Tom Squires 20.02.2012 - 18:57
fuente

7 respuestas

111

Básicamente, la inyección de dependencia hace algunas suposiciones (generalmente pero no siempre válidas) sobre la naturaleza de sus objetos. Si esos son incorrectos, DI puede no ser la mejor solución:

  • Primero, básicamente, DI supone que el acoplamiento apretado de implementaciones de objetos SIEMPRE es malo . Esta es la esencia del Principio de Inversión de Dependencia: "una dependencia nunca debe hacerse sobre una concreción, solo sobre una abstracción".

    Esto cierra el objeto dependiente para cambiar en función de un cambio en la implementación concreta; una clase que depende de ConsoleWriter deberá cambiar específicamente si la salida necesita ir a un archivo en su lugar, pero si la clase dependía solo de que IWriter expusiera un método Write (), podemos reemplazar el ConsoleWriter que se está utilizando actualmente con un FileWriter y nuestro La clase dependiente no sabría la diferencia (principio de sustitución de Liskhov).

    Sin embargo, un diseño NUNCA puede estar cerrado a todos los tipos de cambio; Si el diseño de la interfaz de IWriter cambia, para agregar un parámetro a Write (), ahora se debe cambiar un objeto de código adicional (la interfaz de IWriter), encima del objeto / método de implementación y su uso (s). Si los cambios en la interfaz real son más probables que los cambios en la implementación de dicha interfaz, el acoplamiento suelto (y las dependencias de acoplamiento débil) pueden causar más problemas de los que resuelve.

  • Segundo, y corolario, DI supone que la clase dependiente NUNCA es un buen lugar para crear una dependencia . Esto va al principio de responsabilidad única; Si tiene un código que crea una dependencia y también la usa, entonces hay dos razones por las cuales la clase dependiente puede tener que cambiar (un cambio en el uso O la implementación), violando el SRP.

    Sin embargo, una vez más, agregar capas de indirección para DI puede ser una solución a un problema que no existe; Si es lógico encapsular la lógica en una dependencia, pero esa lógica es la única implementación de una dependencia, entonces es más doloroso codificar la resolución de la dependencia (inyección, ubicación de servicio, fábrica) acoplada de manera flexible que lo que sería solo usa new y olvídalo.

  • Por último, DI por su naturaleza centraliza el conocimiento de todas las dependencias Y sus implementaciones . Esto aumenta el número de referencias que debe tener el conjunto que realiza la inyección y, en la mayoría de los casos, NO reduce el número de referencias requeridas por los conjuntos de clases dependientes reales.

    ALGO, EN ALGUNA PARTE, debe tener conocimiento del dependiente, la interfaz de la dependencia y la implementación de la dependencia para poder "conectar los puntos" y satisfacer esa dependencia. El DI tiende a colocar todo ese conocimiento en un nivel muy alto, ya sea en un contenedor IoC o en el código que crea objetos "principales", como el formulario principal o el Controlador, que debe hidratar (o proporcionar métodos de fábrica para) las dependencias. Esto puede poner una gran cantidad de código acoplado estrechamente y muchas referencias de ensamblaje a altos niveles de su aplicación, que solo necesita este conocimiento para "ocultarlo" de las clases dependientes reales (que desde una perspectiva muy básica es la El mejor lugar para tener este conocimiento; donde se usa).

    Normalmente, también no elimina dichas referencias de abajo hacia abajo en el código; un dependiente aún debe hacer referencia a la biblioteca que contiene la interfaz para su dependencia, que se encuentra en uno de los tres lugares:

    • todo en un único ensamblaje de "interfaces" que se vuelve muy centrado en la aplicación,
    • cada uno junto con la (s) implementación (es) principal (es), eliminando la ventaja de no tener que recompilar a los dependientes cuando cambian las dependencias, o
    • uno o dos cada uno en ensamblajes altamente cohesivos, que aumentan el número de ensamblajes, aumentan dramáticamente los tiempos de "construcción completa" y disminuyen el rendimiento de la aplicación.

    Todo esto, nuevamente para resolver un problema en lugares donde puede que no haya ninguno.

respondido por el KeithS 21.02.2012 - 20:11
fuente
27

Fuera de los marcos de inyección de dependencia, la inyección de dependencia (a través de la inyección del constructor o la inyección del colocador) es casi un juego de suma cero: disminuye el acoplamiento entre el objeto A y su dependencia B, pero ahora cualquier objeto que necesite una instancia de A debe ahora también construir el objeto B.

Ha reducido ligeramente el acoplamiento entre A y B, pero se ha reducido la encapsulación de A, y se ha incrementado el acoplamiento entre A y cualquier clase que debe construir una instancia de A, al acoplarlas también a las dependencias de A.

Por lo tanto, la inyección de dependencia (sin un marco) es igualmente dañina, ya que es útil.

El costo adicional suele ser fácilmente justificable, sin embargo: si el código del cliente sabe más sobre cómo construir la dependencia que el objeto en sí mismo, la inyección de dependencia realmente reduce el acoplamiento; por ejemplo, un Escáner no sabe mucho acerca de cómo obtener o construir un flujo de entrada para analizar la entrada, o de qué fuente el código del cliente quiere analizar la entrada, por lo que la inyección del constructor de un flujo de entrada es la solución obvia. p>

Las pruebas son otra justificación, para poder utilizar dependencias simuladas. Eso debería significar agregar un constructor adicional utilizado solo para pruebas que permita inyectar dependencias: si en cambio, cambia sus constructores para que siempre requieran inyectar dependencias, de repente, debe conocer las dependencias de sus dependencias para poder construir sus dependencias. dependencias directas, y usted no puede hacer ningún trabajo.

Puede ser útil, pero definitivamente debería preguntarse por cada dependencia, ¿vale la pena el costo de la prueba y realmente voy a querer burlarme de esta dependencia durante la prueba?

Cuando se agrega un marco de inyección de dependencia y la construcción de dependencias se delega no al código del cliente, sino al marco, el análisis de costo / beneficio cambia enormemente.

En un marco de inyección de dependencia, las compensaciones son un poco diferentes; lo que está perdiendo al inyectar una dependencia es la capacidad de saber fácilmente en qué implementación está confiando, y cambiar la responsabilidad de decidir en qué dependencia está confiando en algún proceso de resolución automatizado (por ejemplo, si necesitamos un Foo @ inyectado). , debe haber algo que @Provides Foo, y cuyas dependencias inyectadas estén disponibles), o para algún archivo de configuración de alto nivel que indique qué proveedor se debe usar para cada recurso, o para algún híbrido de los dos (por ejemplo, puede ser un proceso de resolución automatizado para dependencias que pueden ser anuladas, si es necesario, usando un archivo de configuración).

Al igual que en la inyección de constructores, creo que la ventaja de hacerlo termina siendo, de nuevo, muy similar al costo de hacerlo: no tiene que saber quién proporciona los datos en los que confía y, si los hay. Si hay varios proveedores potenciales, no tiene que saber cuál es el orden preferido para buscar proveedores, asegúrese de que todas las ubicaciones que necesitan los datos verifiquen a todos los proveedores potenciales, etc., porque todo eso se maneja a un alto nivel. La plataforma de inyección de dependencias.

Aunque personalmente no tengo mucha experiencia con los marcos DI, mi impresión es que ofrecen más beneficios que costos cuando el dolor de cabeza de encontrar el proveedor correcto de los datos o servicios que necesita tiene un costo mayor que el dolor de cabeza, cuando algo falla, de no saber de manera local qué código proporcionó los datos incorrectos que causaron una falla posterior en su código.

En algunos casos, otros patrones que ocultan las dependencias (p. ej., localizadores de servicios) ya se habían adoptado (y quizás también han demostrado su valor) cuando aparecieron los marcos DI en la escena, y se adoptaron los marcos DI porque ofrecían alguna ventaja competitiva. como requerir menos código repetitivo, o posiblemente hacer menos para ocultar al proveedor la dependencia cuando sea necesario determinar qué proveedor está realmente en uso.

    
respondido por el Theodore Murdock 21.02.2012 - 01:39
fuente
11
  1. si está creando entidades de base de datos, debería tener una clase de fábrica que inyectará en su controlador,

  2. si necesita crear objetos primitivos como ints o largos. También debe crear "a mano" la mayoría de los objetos estándar de la biblioteca como fechas, guids, etc.

  3. si desea inyectar cadenas de configuración, probablemente sea mejor idea inyectar algunos objetos de configuración (en general, se recomienda incluir tipos simples en objetos significativos: int temperatureInCelsiusDegrees - > CelciusDeegree temperature)

Y no use el Localizador de servicios como una alternativa de inyección de dependencia, es anti-patrón, más información: enlace

    
respondido por el 0lukasz0 20.02.2012 - 23:09
fuente
8

Cuando no puede ganar nada al hacer que su proyecto sea mantenible y comprobable.

En serio, me encanta IoC y DI en general, y diría que el 98% del tiempo usaré ese patrón sin fallar. Es especialmente importante en un entorno multiusuario, donde el código puede ser reutilizado una y otra vez por diferentes miembros del equipo y diferentes proyectos, ya que separa la lógica de la implementación. El registro es un buen ejemplo de esto, una interfaz ILog inyectada en una clase es mil veces más fácil de mantener que simplemente conectando su marco de trabajo de registro-du-jour, ya que no tiene garantía de que otro proyecto usará el mismo marco de registro (si lo usa). uno en absoluto!).

Sin embargo, hay ocasiones en que no es un patrón aplicable. Por ejemplo, los puntos de entrada funcionales que se implementan en un contexto estático por un inicializador no reemplazable (WebMethods, lo estoy mirando, pero su método Main () en su clase de programa es otro ejemplo) simplemente no puede tener dependencias inyectadas en la inicialización hora. También me atrevería a decir que un prototipo, o cualquier código de investigación desechable, también es un mal candidato; los beneficios de la DI son beneficios a mediano y largo plazo (capacidad de prueba y facilidad de mantenimiento), si está seguro de que desechará la mayoría de un código dentro de una semana o así, diría que no gana nada aislando dependencias, solo dedique el tiempo que normalmente pasaría probando y aislando dependencias para que el código funcione.

En definitiva, es sensato adoptar un enfoque pragmático de cualquier metodología o patrón, ya que nada es aplicable el 100% del tiempo.

Una cosa a tener en cuenta es su comentario acerca de las pruebas automatizadas: mi definición de esto es pruebas funcionales automatizadas, por ejemplo, pruebas de selenio con script si se encuentra en un contexto web. En general, estas son pruebas de caja negra, sin necesidad de conocer el funcionamiento interno del código. Si se estuviera refiriendo a pruebas de Unidad o Integración, diría que el patrón DI casi siempre es aplicable a cualquier proyecto que se basa en gran medida en ese tipo de pruebas de caja blanca, ya que, por ejemplo, le permite probar cosas como métodos que tocan la base de datos sin necesidad de que una base de datos esté presente.

    
respondido por el Ed James 21.02.2012 - 20:42
fuente
1

Esta no es una respuesta completa, sino solo otro punto.

Cuando tienes una aplicación que se inicia una vez, se ejecuta durante mucho tiempo (como una aplicación web) DI podría ser buena.

Cuando tienes una aplicación que se inicia muchas veces y se ejecuta durante tiempos más cortos (como una aplicación móvil) probablemente no quieras el contenedor.

    
respondido por el Koray Tugay 16.09.2016 - 09:28
fuente
0

Mientras que las otras respuestas se centran en los aspectos técnicos, me gustaría agregar una dimensión práctica.

A lo largo de los años, llegué a la conclusión de que hay varios requisitos prácticos que deben cumplirse para que la introducción de la inyección de dependencia sea un éxito.

  1. Debe haber una razón para introducirlo.

    Esto suena obvio, pero si su código solo obtiene elementos de la base de datos y los devuelve sin ninguna lógica, agregar un contenedor DI hace que las cosas sean más complejas sin un beneficio real. Las pruebas de integración serían más importantes aquí.

  2. El equipo debe estar entrenado y a bordo.

    A menos que la mayoría del equipo esté a bordo y comprenda que DI, la inversión del contenedor de control se convierte en otra forma de hacer las cosas y hace que el código base sea aún más complicado.

    Si un miembro nuevo del equipo presenta el DI, porque lo entienden y les gusta y solo quieren mostrar que son buenos, Y el equipo no participa activamente, existe un riesgo real de que realmente Disminuir la calidad del código.

  3. Necesitas probar

    Aunque el desacoplamiento es generalmente bueno, DI puede mover la resolución de una dependencia desde el tiempo de compilación hasta el tiempo de ejecución. Esto es bastante peligroso si no se prueba bien. Las fallas en la resolución del tiempo de ejecución pueden ser costosas de rastrear y resolver.
    (De su prueba queda claro que lo hace, pero muchos equipos no lo hacen en la medida en que lo requiere DI).

respondido por el tymtam 16.09.2016 - 02:27
fuente
-1

Una alternativa a la inyección de dependencia es usar un Localizador de servicios . Un localizador de servicios es más fácil de entender, depurar y simplifica la construcción de un objeto, especialmente si no está utilizando un marco DI. Los localizadores de servicios son un buen patrón para administrar las dependencias estáticas externas, por ejemplo, una base de datos que de otro modo tendría que pasar a cada objeto en su capa de acceso a datos.

Cuando refactoriza el código heredado , a menudo es más fácil refactorizar a un Localizador de servicio que a Inyección de dependencia. Todo lo que hace es reemplazar las instancias con búsquedas de servicio y luego falsificar el servicio en su prueba de unidad.

Sin embargo, hay algunas desventajas en el Localizador de servicios. Conocer las dependencias de una clase es más difícil porque las dependencias están ocultas en la implementación de la clase, no en los constructores ni en los establecedores. Y crear dos objetos que se basan en diferentes implementaciones del mismo servicio es difícil o imposible.

TLDR : si su clase tiene dependencias estáticas o si está refactorizando el código heredado, un Localizador de Servicios es posiblemente mejor que DI.

    
respondido por el Garrett Hall 20.02.2012 - 19:54
fuente

Lea otras preguntas en las etiquetas