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

124

Desde que aprendí (y me encantó) las pruebas automatizadas, me encontré 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?

Tom Squires
fuente
1
Relacionado (probablemente no duplicado) - stackoverflow.com/questions/2407540/…
Steve314
3
Suena un poco como el antipatrón Golden Hammer. en.wikipedia.org/wiki/Anti-pattern
Cuga

Respuestas:

123

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

  • Primero, más básicamente, DI supone que el acoplamiento estrecho de las implementaciones de objetos SIEMPRE es malo . Esta es la esencia del Principio de Inversión de Dependencia: "nunca se debe hacer una dependencia de una concreción; solo de una abstracción".

    Esto cierra el objeto dependiente al cambio basado en un cambio en la implementación concreta; una clase que dependa de ConsoleWriter específicamente tendrá que cambiar si la salida necesita ir a un archivo, pero si la clase dependía solo de un IWriter que exponga 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 cerrarse a todo tipo de cambio; Si el diseño de la interfaz IWriter cambia, para agregar un parámetro a Write (), ahora se debe cambiar un objeto de código adicional (la interfaz IWriter), además del objeto / método de implementación y sus usos. Si los cambios en la interfaz real son más probables que los cambios en la implementación de dicha interfaz, el acoplamiento flojo (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 que la clase dependiente puede tener que cambiar (un cambio en el uso O la implementación), violando 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 del servicio, fábrica) de manera flexible. simplemente usarlo newy olvidarlo.

  • 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 dependencia y la implementación de la dependencia para "conectar los puntos" y satisfacer esa dependencia. DI tiende a colocar todo ese conocimiento en un nivel muy alto, ya sea en un contenedor de IoC o en el código que crea objetos "principales" como la forma principal o el Controlador que debe hidratar (o proporcionar métodos de fábrica para) las dependencias. Esto puede colocar una gran cantidad de código necesariamente estrechamente acoplado y muchas referencias de ensamblaje en los 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 mejor lugar para tener este conocimiento; dónde se usa).

    Tampoco normalmente elimina dichas referencias desde abajo en el código; un dependiente aún debe hacer referencia a la biblioteca que contiene la interfaz para su dependencia, que está en uno de tres lugares:

    • todo en un solo 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 volver a compilar dependientes cuando las dependencias cambian, o
    • uno o dos por unidad en ensamblajes altamente cohesivos, que aumentan el recuento de ensamblajes, aumenta drásticamente los tiempos de "compilación completa" y disminuye el rendimiento de la aplicación.

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

KeithS
fuente
1
SRP = principio de responsabilidad única, para cualquiera que se pregunte.
Theodore Murdock
1
Sí, y LSP es el principio de sustitución de Liskov; dada una dependencia de A, la dependencia debería poder ser satisfecha por B, que se deriva de A sin ningún otro cambio.
KeithS
la inyección de dependencia ayuda específicamente donde necesita obtener la clase A de la clase B de la clase D etc. Además, nunca tuve un cuello de botella causado por DI ... piense en el costo de mantenimiento, nunca en el costo de la CPU porque el código siempre se puede optimizar, pero hacerlo sin una razón tiene un costo
GameDeveloper
¿Sería otra suposición "si una dependencia cambia, cambia en todas partes"? O de lo contrario hay que mirar todas las clases de consumo de todos modos
Richard Tingle
3
Esta respuesta no aborda el impacto de la capacidad de prueba de inyectar dependencias en lugar de codificarlas. Consulte también el Principio de dependencias explícitas ( deviq.com/explicit-dependencies-principle )
ssmith
30

Fuera de los marcos de inyección de dependencia, la inyección de dependencia (mediante inyección de constructor o inyección de setter) es casi un juego de suma cero: se reduce el acoplamiento entre el objeto A y su dependencia B, pero ahora cualquier objeto que necesite una instancia de A ahora debe también construye el objeto B.

Redujo ligeramente el acoplamiento entre A y B, pero redujo la encapsulación de A y aumentó el acoplamiento entre A y cualquier clase que deba 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.

Sin embargo, el costo adicional a menudo es fácilmente justificable: si el código del cliente sabe más sobre cómo construir la dependencia que el objeto mismo, entonces la inyección de dependencia realmente reduce el acoplamiento; por ejemplo, un escáner no sabe mucho acerca de cómo obtener o construir una secuencia de entrada para analizar la entrada, o de qué fuente quiere que el código del cliente analice la entrada, por lo que la inyección del constructor de una secuencia de entrada es la solución obvia.

La prueba es otra justificación para poder utilizar dependencias simuladas. Eso debería significar agregar un constructor adicional utilizado para probar solo que permita la inyección de dependencias: si en cambio cambia sus constructores para que siempre requieran inyecciones de dependencias, de repente, debe conocer las dependencias de las dependencias de sus dependencias para construir su dependencias directas, y no puedes hacer ningún trabajo.

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

Cuando se agrega un marco de inyección de dependencias, 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 a algún proceso de resolución automatizado (por ejemplo, si necesitamos un Foo @ Inject'ed , debe haber algo que @Provides Foo, y cuyas dependencias inyectadas estén disponibles), o algún archivo de configuración de alto nivel que prescriba qué proveedor debe usarse para cada recurso, o algún híbrido de los dos (por ejemplo, puede ser un proceso de resolución automatizado para dependencias que se pueden anular, 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, nuevamente, siendo muy similar al costo de hacerlo: no tiene que saber quién proporciona los datos en los que confía y, si hay múltiples posibilidades proveedores, no tiene que conocer el orden preferido para buscar proveedores, asegúrese de que cada ubicación que necesita los datos verifique para todos los proveedores potenciales, etc., porque todo eso se maneja en un alto nivel por la inyección de dependencia plataforma.

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

En algunos casos, otros patrones que oscurecen las dependencias (por ejemplo, localizadores de servicios) ya se habían adoptado (y tal vez también demostraron su valía) cuando aparecieron los marcos DI en la escena, y los marcos DI se adoptaron porque ofrecían alguna ventaja competitiva, como requerir menos código repetitivo, o potencialmente haciendo menos para ocultar al proveedor de la dependencia cuando sea necesario determinar qué proveedor está realmente en uso.

Theodore Murdock
fuente
66
Solo un comentario rápido sobre la burla de dependencias en las pruebas y "tienes que saber sobre las dependencias de tus dependencias" ... eso no es cierto en absoluto. Uno no necesita saber o preocuparse por algunas dependencias de implementación concretas si se está inyectando un simulacro. Uno solo necesita saber acerca de las dependencias directas de la clase bajo prueba, que son satisfechas por los simulacros.
Eric King
66
No lo entiendes, no estoy hablando cuando inyectas un simulacro, estoy hablando del código real. Considere la clase A con dependencia B, que a su vez tiene dependencia C, que a su vez tiene dependencia D. Sin DI, A construye B, B construye C, C construye D. Con inyección de construcción, para construir A, primero debe construir B, para construir B primero debes construir C, y para construir C primero debes construir D. Entonces, la clase A ahora tiene que saber sobre D, la dependencia de una dependencia de una dependencia, para construir B. Esto lleva a un acoplamiento excesivo.
Theodore Murdock
1
No habría tanto costo tener un constructor adicional utilizado para probar solo que permita inyectar las dependencias. Trataré de revisar lo que dije.
Theodore Murdock
3
La inyección de dependencia no solo mueve las dependencias en un juego de suma cero. Mueves tus dependencias a lugares donde ya existen: el proyecto principal. Incluso puede usar enfoques basados ​​en convenciones para asegurarse de que las dependencias solo se resuelvan en tiempo de ejecución, por ejemplo, especificando qué clases son concretas por espacios de nombres o lo que sea. Tengo que decir que esta es una respuesta ingenua
Sprague
2
La inyección de dependencia de @Sprague se mueve alrededor de las dependencias en un juego de suma ligeramente negativa, si lo aplica en los casos en que no debería aplicarse. El propósito de esta respuesta es recordar a las personas que la inyección de dependencia no es inherentemente buena, es un patrón de diseño que es increíblemente útil en las circunstancias correctas, pero un costo injustificable en circunstancias normales (al menos en los dominios de programación en los que he trabajado , Entiendo que en algunos dominios es más útil que en otros). DI a veces se convierte en una palabra de moda y un fin en sí mismo, lo que pierde el punto.
Theodore Murdock
11
  1. si está creando entidades de base de datos, debería tener alguna clase de fábrica que inyectará en su controlador,

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

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

Y no use el Localizador de servicios como alternativa de inyección de dependencia, es antipatrón, más información: http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx

0lukasz0
fuente
Todos los puntos son sobre el uso de diferentes formas de inyección en lugar de no usarlo por completo. +1 para el enlace sin embargo.
Jan Hudec
3
Service Locator NO es un antipatrón y el tipo al que enlazó en el blog no es un experto en patrones. El hecho de que se hizo famoso por StackExchange es una especie de perjuicio para el diseño de software. Martin Fowler (autor de los patrones básicos de la arquitectura de aplicaciones empresariales) tiene una visión más razonable al respecto: martinfowler.com/articles/injection.html
Colin
1
@Colin, por el contrario, Service Locator es definitivamente un antipatrón, y aquí en pocas palabras es la razón .
Kyralessa
@ Kyralessa: se da cuenta de que los marcos DI utilizan internamente un patrón de Localizador de servicios para buscar servicios, ¿verdad? Hay circunstancias en las que ambos son apropiados, y ninguno de los dos es un anti patrón a pesar del odio de StackExchange que algunas personas quieren deshacerse.
Colin
Claro, y mi automóvil usa internamente pistones y bujías y una serie de otras cosas que, sin embargo, no son una buena interfaz para un ser humano común que solo quiere conducir un automóvil.
Kyralessa
8

Cuando no puede ganar nada haciendo que su proyecto sea sostenible y comprobable.

En serio, me encanta IoC y DI en general, y diría que el 98% de las veces usaré ese patrón sin falta. 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 excelente ejemplo de esto, una interfaz ILog inyectada en una clase es mil veces más fácil de mantener que simplemente enchufar su marco de registro-du-jour, ya que no tiene garantía de que otro proyecto use el mismo marco de registro (si usa uno en absoluto!).

Sin embargo, hay momentos 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, te estoy mirando, pero tu método Main () en tu clase de Programa es otro ejemplo) simplemente no pueden tener dependencias inyectadas en la inicialización hora. También iría tan lejos como para decir que un prototipo, o cualquier código de investigación desechable, también es un mal candidato; los beneficios de la DI son más o menos beneficios a mediano y largo plazo (comprobabilidad y mantenibilidad), si está seguro de que eliminará la mayor parte de un código dentro de una semana más o menos, diría que no gana nada aislando dependencias, solo pase el tiempo que normalmente pasaría probando y aislando dependencias para que el código funcione.

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

Una cosa a tener en cuenta es su comentario sobre las pruebas automatizadas: mi definición de esto es pruebas funcionales automatizadas , por ejemplo, pruebas de selenio programadas si se encuentra en un contexto web. Estas son generalmente pruebas de caja negra, sin necesidad de saber sobre el funcionamiento interno del código. Si te refieres a las pruebas de Unidad o Integración, diría que el patrón DI es casi siempre aplicable a cualquier proyecto que dependa en gran medida de ese tipo de pruebas de caja blanca, ya que, por ejemplo, te permite probar cosas como métodos que tocan la base de datos sin necesidad de que haya una base de datos presente.

Ed James
fuente
No entiendo a qué te refieres con el registro. Seguramente, todos los registradores no se ajustarán a la misma interfaz, por lo que tendría que escribir su propio contenedor para traducir entre esta forma de registro de proyectos y un registrador específico (suponiendo que quisiera poder cambiar fácilmente los registradores). Entonces, después de eso, ¿qué te da DI?
Richard Tingle
@RichardTingle Yo diría que debes definir tu contenedor de registro como una interfaz, y luego escribir una implementación ligera de esa interfaz para cada servicio de registro externo, en lugar de usar una sola clase de registro que envuelva múltiples abstracciones. Es el mismo concepto que está proponiendo, pero abstraído en un nivel diferente.
Ed James
1

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

Con 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. Debería haber una razón para presentarlo.

    Esto suena obvio, pero si su código solo obtiene cosas de la base de datos y lo devuelve sin ninguna lógica, entonces 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 necesita ser entrenado y a bordo.

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

    Si el DI es presentado por un nuevo miembro del equipo, porque lo entienden y les gusta y solo quieren demostrar que son buenos, Y el equipo no está involucrado activamente, existe un riesgo real de que realmente disminuya la calidad de el código.

  3. Necesitas probar

    Si bien el desacoplamiento generalmente es algo bueno, DI puede mover la resolución de una dependencia del tiempo de compilación al tiempo de ejecución. En realidad, esto es bastante peligroso si no pruebas bien. Las fallas de resolución del tiempo de ejecución pueden ser costosas de rastrear y resolver.
    (Está claro en su prueba que lo hace, pero muchos equipos no prueban en la medida que lo requiere DI).

tymtam
fuente
1

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

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

Cuando tiene una aplicación que se inicia muchas veces y se ejecuta por períodos más cortos (como una aplicación móvil), probablemente no desee el contenedor.

Koray Tugay
fuente
No veo cómo el tiempo de aplicación en vivo se relacionó con DI
Michael Freidgeim
@MichaelFreidgeim Toma tiempo inicializar el contexto, generalmente los contenedores DI son bastante pesados, como Spring. Haga una aplicación hello world con solo una clase y haga una con Spring y comience ambas 10 veces y verá lo que quiero decir.
Koray Tugay
Suena como un problema con un contenedor DI individual. En el mundo .Net no he oído hablar del tiempo de inicialización como un problema esencial para los contenedores DI
Michael Freidgeim,
1

Trate de usar los principios básicos de OOP: use la herencia para extraer funcionalidades comunes, encapsular (ocultar) cosas que deben protegerse del mundo exterior usando miembros / tipos privados / internos / protegidos. Use cualquier potente marco de prueba para inyectar código solo para pruebas, por ejemplo https://www.typemock.com/ o https://www.telerik.com/products/mocking.aspx .

Luego intente reescribirlo con DI y compare el código, lo que verá generalmente con DI:

  1. Tienes más interfaces (más tipos)
  2. Ha creado duplicados de firmas de métodos públicos y tendrá que mantenerlo dos veces (no puede simplemente cambiar algún parámetro una vez, tendrá que hacerlo dos veces, básicamente todas las posibilidades de refactorización y navegación se vuelven más complicadas)
  3. Ha movido algunos errores de compilación para fallas en el tiempo de ejecución (con DI puede ignorar alguna dependencia durante la codificación y no está seguro de que quedará expuesto durante las pruebas)
  4. Has abierto tu encapsulación. Ahora los miembros protegidos, los tipos internos, etc., se hicieron públicos.
  5. Se aumentó la cantidad total de código

Diría que, por lo que vi, casi siempre, la calidad del código se reduce con DI.

Sin embargo, si usa solo un modificador de acceso "público" en la declaración de clase, y / o modificadores públicos / privados para los miembros, y / o no tiene la opción de comprar herramientas de prueba costosas y al mismo tiempo necesita pruebas unitarias que puedan " No puede ser reemplazado por pruebas integrales, y / o si ya tiene interfaces para las clases que piensa inyectar, ¡DI es una buena opción!

ps probablemente obtendré muchas desventajas para esta publicación, creo que porque la mayoría de los desarrolladores modernos simplemente no entienden cómo y por qué usar palabras clave internas y cómo disminuir el acoplamiento de sus componentes y finalmente por qué disminuirlo) finalmente, solo intenta codificar y comparar

Eugene Gorbovoy
fuente
0

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 dependencias estáticas externas , por ejemplo, una base de datos que de lo contrario tendría que pasar a cada objeto en su capa de acceso a datos.

Cuando se refactoriza el código heredado , a menudo es más fácil refactorizar a un Localizador de servicios que a la Inyección de dependencias. Todo lo que debe hacer es reemplazar las instancias con búsquedas de servicio y luego simular el servicio en la prueba de su 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 constructores o setters. Y crear dos objetos que dependen de diferentes implementaciones del mismo servicio es difícil o imposible.

TLDR : si su clase tiene dependencias estáticas o está refactorizando el código heredado, podría decirse que un Localizador de servicios es mejor que DI.

Garrett Hall
fuente
12
El Localizador de servicios oculta las dependencias en su código . No es un buen patrón para usar.
Kyralessa
Cuando se trata de dependencias estáticas, prefiero ver las fachadas que implementan interfaces. Estos pueden ser inyectados de dependencia, y caen en la granada estática por ti.
Erik Dietrich
@ Kyralessa Estoy de acuerdo, un Localizador de servicios tiene muchas desventajas y casi siempre es preferible DI. Sin embargo, creo que hay un par de excepciones a la regla, como con todos los principios de programación.
Garrett Hall
El principal uso aceptado de la ubicación del servicio se encuentra dentro de un patrón de Estrategia, donde una clase de "selector de estrategia" recibe suficiente lógica para encontrar la estrategia a usar, y puede devolverla o pasarle una llamada. Incluso en este caso, el selector de estrategia puede ser una fachada para un método de fábrica proporcionado por un contenedor de IoC, que tiene la misma lógica; la razón por la que lo separarías es para poner la lógica donde pertenece y no donde está más oculta.
KeithS
Para citar a Martin Fowler, "La elección entre (DI y Service Locator) es menos importante que el principio de separar la configuración del uso". La idea de que DI SIEMPRE es mejor es la tontería. Si un servicio se usa globalmente, es más engorroso y frágil usar DI. Además, DI es menos favorable para arquitecturas de complementos donde las implementaciones no se conocen hasta el tiempo de ejecución. ¡Piense lo incómodo que sería pasar siempre argumentos adicionales a Debug.WriteLine () y similares!
Colin