Inyección de dependencias / prácticas de contenedor de IoC al escribir marcos

18

He utilizado varios contenedores IoC (Castle.Windsor, Autofac, MEF, etc.) para .Net en varios proyectos. He descubierto que tienden a ser abusados ​​con frecuencia y alientan una serie de malas prácticas.

¿Existen prácticas establecidas para el uso de contenedores IoC, particularmente cuando se proporciona una plataforma / marco? Mi objetivo como escritor de marcos es hacer que el código sea lo más simple y fácil de usar posible. Prefiero escribir una línea de código para construir un objeto que diez o incluso solo dos.

Por ejemplo, un par de olores de código que he notado y no tengo buenas sugerencias para:

  1. Gran cantidad de parámetros (> 5) para constructores. Crear servicios tiende a ser complejo; Todas las dependencias se inyectan a través del constructor, a pesar de que los componentes rara vez son opcionales (excepto tal vez en las pruebas).

  2. Falta de clases privadas e internas; Esta puede ser una limitación específica del uso de C # y Silverlight, pero estoy interesado en cómo se resuelve. Es difícil saber qué es una interfaz de framework si todas las clases son públicas; me permite acceder a partes privadas que probablemente no debería tocar.

  3. Acoplamiento del ciclo de vida del objeto al contenedor de IoC. A menudo es difícil construir manualmente las dependencias necesarias para crear objetos. El marco de trabajo de IoC gestiona con demasiada frecuencia el ciclo de vida del objeto He visto proyectos donde la mayoría de las clases están registradas como Singletons. Obtiene una falta de control explícito y también se ve obligado a administrar los elementos internos (se relaciona con el punto anterior, todas las clases son públicas y debe inyectarlas).

Por ejemplo, .Net framework tiene muchos métodos estáticos. como DateTime.UtcNow. Muchas veces he visto esto envuelto e inyectado como un parámetro de construcción.

Dependiendo de la implementación concreta, mi código es difícil de probar. Inyectar una dependencia hace que mi código sea difícil de usar, especialmente si la clase tiene muchos parámetros.

¿Cómo proporciono una interfaz comprobable y una fácil de usar? ¿Cuáles son las mejores prácticas?

Dave Hillier
fuente
1
¿Qué malas prácticas fomentan? En cuanto a las clases privadas, no debería necesitar tener muchas de ellas si tiene una buena encapsulación; por un lado, son mucho más difíciles de probar.
Aaronaught
Err, 1 y 2 son las malas prácticas. ¿Constructores grandes y que no usan encapsulación para establecer una interfaz mínima? Además, dije privado e interno (use elementos internos visibles para, para probar). Nunca he usado un marco que me obligue a usar un contenedor IoC particular.
Dave Hillier
2
¿Es posible que tal vez una señal de que necesite muchos parámetros para los constructores de su clase sea en sí mismo el olor del código y el DIP lo esté haciendo más obvio?
dreza
3
Usar un contenedor de IoC específico en un marco generalmente no es una buena práctica. Usar inyección de dependencia es una buena práctica. ¿Eres consciente de la diferencia?
Aaronaught
1
@Aaronaught (de vuelta de entre los muertos). El uso de "la inyección de dependencia es una buena práctica" es falso. La inyección de dependencia es una herramienta que debe usarse solo cuando sea apropiado. Usar inyección de dependencia todo el tiempo es una mala práctica.
Dave Hillier

Respuestas:

27

El único anti-patrón legítimo de inyección de dependencia que conozco es el Localizador de servicios patrón , que es un antipatrón cuando se usa un marco DI para él.

Todos los otros llamados antipatrones de DI que he escuchado, aquí o en otros lugares, son casos un poco más específicos de antipatrones generales de diseño de software / OO. Por ejemplo:

  • La sobreinyección del constructor es una violación del Principio de responsabilidad única . Demasiados argumentos de constructor indican demasiadas dependencias; Demasiadas dependencias indican que la clase está tratando de hacer demasiado. Por lo general, este error se correlaciona con otros olores de código, como nombres de clase inusualmente largos o ambiguos ("administrador"). Las herramientas de análisis estático pueden detectar fácilmente el acoplamiento aferente / eferente excesivo.

  • La inyección de datos, en oposición al comportamiento, es un subtipo del antipatrón poltergeist , siendo el 'geist en este caso el contenedor. Si una clase necesita estar al tanto de la fecha y hora actuales, no se inyecta un DateTime, que es datos; en su lugar, inyecta una abstracción sobre el reloj del sistema (generalmente llamo al mío ISystemClock, aunque creo que hay uno más general en el proyecto SystemWrappers ). Esto no solo es correcto para DI; es absolutamente esencial para la capacidad de prueba, para que pueda probar funciones que varían en el tiempo sin tener que esperarlas.

  • Declarar cada ciclo de vida como Singleton es, para mí, un ejemplo perfecto de programación de culto de carga y, en menor grado, el " pozo negro de objetos " denominado coloquialmente . He visto más abuso de singleton del que me gustaría recordar, y muy poco de eso involucra DI.

  • Otro error común son los tipos de interfaz específicos de la implementación (con nombres extraños como IOracleRepository) realizados solo para poder registrarlo en el contenedor. Esto es en sí mismo una violación del Principio de Inversión de Dependencia (solo porque es una interfaz, no significa que sea realmente abstracto) y, a menudo, también incluye una hinchazón de la interfaz que viola el Principio de Segregación de la Interfaz .

  • El último error que generalmente veo es la "dependencia opcional", que hicieron en NerdDinner . En otras palabras, hay un constructor que acepta la inyección de dependencia, pero también otro constructor que usa una implementación "predeterminada". Esto también viola el DIP y tiende a conducir a violaciones de LSP , ya que los desarrolladores, con el tiempo, comienzan a hacer suposiciones sobre la implementación predeterminada y / o comienzan instancias nuevas utilizando el constructor predeterminado.

Como dice el viejo dicho, puedes escribir FORTRAN en cualquier idioma . La Inyección de dependencias no es una bala de plata que evitará que los desarrolladores arruinen su administración de dependencias, pero evita una serie de errores / antipatrones comunes:

...y así.

Obviamente, no desea diseñar un marco para depender de una implementación específica del contenedor de IoC , como Unity o AutoFac. Es decir, una vez más, violar el DIP. Pero si te encuentras incluso pensando en hacer algo así, entonces ya debes haber cometido varios errores de diseño, porque la inyección de dependencias es una técnica de gestión de dependencias de propósito general y no vinculada al concepto de un contenedor IoC.

Cualquier cosa puede construir un árbol de dependencia; tal vez es un contenedor de IoC, tal vez es una prueba unitaria con un montón de simulacros, tal vez es un controlador de prueba que proporciona datos ficticios. Su marco no debería importarle, y a la mayoría de los marcos que he visto no les importa, pero aún así hacen un uso intensivo de la inyección de dependencia para que pueda integrarse fácilmente en el contenedor de IoC elegido por el usuario final.

DI no es ciencia espacial. Solo trate de evitar newy staticexcepto cuando haya una razón convincente para usarlos, como un método de utilidad que no tenga dependencias externas o una clase de utilidad que posiblemente no tenga ningún propósito fuera del marco (los contenedores de interoperabilidad y las claves de diccionario son ejemplos comunes de esta).

Muchos de los problemas con los marcos de IoC surgen cuando los desarrolladores aprenden por primera vez cómo usarlos, y en lugar de cambiar realmente la forma en que manejan las dependencias y las abstracciones para adaptarse al modelo de IoC, intentan manipular el contenedor de IoC para cumplir con las expectativas de sus clientes. estilo de codificación antiguo, que a menudo implicaría un alto acoplamiento y baja cohesión. El código incorrecto es un código incorrecto, ya sea que use técnicas DI o no.

Aaronaught
fuente
Tengo algunas preguntas. Para una biblioteca que se distribuye en NuGet, no puedo depender de un sistema IoC, ya que lo hace la aplicación que usa la biblioteca, no la biblioteca en sí. En muchos casos, el usuario de la biblioteca realmente no se preocupa por sus dependencias internas. ¿Hay algún problema con tener un constructor predeterminado que inicie las dependencias internas y un constructor más largo para pruebas unitarias y / o implementaciones personalizadas? También dices para evitar "nuevo". ¿Esto se aplica a cosas como StringBuilder, DateTime y TimeSpan?
Etienne Charland