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?
design-patterns
dependency-injection
Tom Squires
fuente
fuente
Respuestas:
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
new
y 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 esto, nuevamente para resolver un problema en lugares donde puede no haber ninguno.
fuente
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.
fuente
si está creando entidades de base de datos, debería tener alguna clase de fábrica que inyectará en su controlador,
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.
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
fuente
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.
fuente
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.
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í.
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.
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).
fuente
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.
fuente
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:
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
fuente
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.
fuente