¿Cuánto es demasiada inyección de dependencia?

38

Trabajo en un proyecto que usa la inyección de dependencia (Spring) para literalmente todo lo que es una dependencia de una clase. Estamos en un punto donde el archivo de configuración de Spring ha crecido a aproximadamente 4000 líneas. No hace mucho, vi una de las charlas del tío Bob en YouTube (desafortunadamente, no pude encontrar el enlace) en la que recomienda inyectar solo un par de dependencias centrales (por ejemplo, fábricas, bases de datos, ...) en el componente Principal desde el cual luego ser distribuido.

Las ventajas de este enfoque es el desacoplamiento del marco DI de la mayor parte de la aplicación y también hace que la configuración de Spring sea más limpia, ya que las fábricas contendrán mucho más de lo que estaba en la configuración anterior. Por el contrario, esto dará como resultado la difusión de la lógica de creación entre muchas clases de fábrica y las pruebas podrían ser más difíciles.

Entonces mi pregunta realmente es qué otras ventajas o desventajas ves en uno u otro enfoque. ¿Hay alguna mejor práctica? ¡Muchas gracias por sus respuestas!

Antimon
fuente
3
Tener un archivo de configuración de 4000 líneas no es culpa de la inyección de dependencia ... hay muchas maneras de solucionarlo: modularizar el archivo en múltiples archivos más pequeños, cambiar a inyección basada en anotaciones, usar archivos JavaConfig en lugar de xml. Básicamente, el problema que está experimentando es la incapacidad de administrar la complejidad y el tamaño de las necesidades de inyección de dependencia de su aplicación.
Maybe_Factor
1
@Maybe_Factor TL; DR dividió el proyecto en componentes más pequeños.
Walfrat

Respuestas:

44

Como siempre, depende de ™. La respuesta depende del problema que uno está tratando de resolver. En esta respuesta, trataré de abordar algunas fuerzas motivadoras comunes:

Favorecer bases de código más pequeñas

Si tiene 4.000 líneas de código de configuración Spring, supongo que la base del código tiene miles de clases.

No es un problema que pueda resolver después del hecho, pero como regla general, tiendo a preferir aplicaciones más pequeñas, con bases de código más pequeñas. Si te gusta el diseño impulsado por dominio , podrías, por ejemplo, hacer una base de código por contexto acotado.

Baso este consejo en mi limitada experiencia, ya que he escrito código de línea de negocio basado en la web durante la mayor parte de mi carrera. Me imagino que si está desarrollando una aplicación de escritorio, un sistema integrado u otro, las cosas son más difíciles de separar.

Si bien me doy cuenta de que este primer consejo es fácilmente el menos práctico, también creo que es el más importante, y por eso lo incluyo. La complejidad del código varía de forma no lineal (posiblemente exponencial) con el tamaño de la base del código.

Favorecer DI puro

Si bien todavía me doy cuenta de que esta pregunta presenta una situación existente, recomiendo Pure DI . No use un contenedor DI, pero si lo hace, al menos úselo para implementar una composición basada en convenciones .

No tengo ninguna experiencia práctica con Spring, pero supongo que por archivo de configuración , un archivo XML está implícito.

Configurar dependencias usando XML es lo peor de ambos mundos. Primero, pierde la seguridad de tipo de tiempo de compilación, pero no gana nada. Un archivo de configuración XML puede ser fácilmente tan grande como el código que intenta reemplazar.

En comparación con el problema que pretende abordar, los archivos de configuración de inyección de dependencia ocupan el lugar equivocado en el reloj de complejidad de la configuración .

El caso de la inyección de dependencia de grano grueso

Puedo hacer un caso para la inyección de dependencia de grano grueso. También puedo hacer un caso para la inyección de dependencia de grano fino (ver la siguiente sección).

Si solo inyecta algunas dependencias 'centrales', la mayoría de las clases podrían verse así:

public class Foo
{
    private readonly Bar bar;

    public Foo()
    {
        this.bar = new Bar();
    }

    // Members go here...
}

Esto es todavía le queda Design Patterns 's favor composición de objetos sobre la herencia de clases , porque FooCompone Bar. Desde una perspectiva de mantenibilidad, esto aún podría considerarse mantenible, porque si necesita cambiar la composición, simplemente edite el código fuente Foo.

Esto es apenas menos sostenible que la inyección de dependencia. De hecho, diría que es más fácil editar directamente la clase que usa Bar, en lugar de tener que seguir la indirecta inherente a la inyección de dependencia.

En la primera edición de mi libro sobre Inyección de dependencias , hago la distinción entre dependencias volátiles y estables.

Las dependencias volátiles son aquellas que debe considerar inyectarse. Incluyen

  • Dependencias que deben ser reconfigurables después de la compilación
  • Dependencias desarrolladas en paralelo por otro equipo
  • Dependencias con comportamiento no determinista, o comportamiento con efectos secundarios.

Las dependencias estables, por otro lado, son dependencias que se comportan de manera bien definida. En cierto sentido, podría argumentar que esta distinción justifica la inyección de dependencia de grano grueso, aunque debo admitir que no me di cuenta por completo cuando escribí el libro.

Sin embargo, desde una perspectiva de prueba, esto hace que las pruebas unitarias sean más difíciles. Ya no puede realizar pruebas unitarias Fooindependientes de Bar. Como explica JB Rainsberger , las pruebas de integración sufren una explosión combinatoria de complejidad. Literalmente, tendrá que escribir decenas de miles de casos de prueba si desea cubrir todas las rutas a través de una integración de incluso 4-5 clases.

El contraargumento a eso es que a menudo, su tarea no es programar una clase. Su tarea es desarrollar un sistema que resuelva algunos problemas específicos. Esta es la motivación detrás de Behavior-Driven Development (BDD).

DHH presenta otro punto de vista sobre esto, quien afirma que TDD conduce a un daño de diseño inducido por la prueba . También favorece las pruebas de integración de grano grueso.

Si toma esta perspectiva en el desarrollo de software, entonces la inyección de dependencia de grano grueso tiene sentido.

El caso de la inyección de dependencia fina

La inyección de dependencia de grano fino, por otro lado, podría describirse como inyectar todas las cosas.

Mi principal preocupación con respecto a la inyección de dependencia de grano grueso es la crítica expresada por JB Rainsberger. No puede cubrir todas las rutas de código con pruebas de integración, porque necesita escribir literalmente miles, o decenas de miles, de casos de prueba para cubrir todas las rutas de código.

Los defensores de BDD responderán con el argumento de que no necesita cubrir todas las rutas de código con pruebas. Solo necesita cubrir aquellos que producen valor comercial.

Sin embargo, en mi experiencia, todas las rutas de código 'exóticas' también se ejecutarán en una implementación de alto volumen, y si no se prueban, muchas de ellas tendrán defectos y causarán excepciones de tiempo de ejecución (a menudo excepciones de referencia nula).

Esto me ha hecho favorecer la inyección de dependencia de grano fino, porque me permite probar las invariantes de todos los objetos de forma aislada.

Favorecer la programación funcional

Si bien me inclino hacia la inyección de dependencia de grano fino, he cambiado mi énfasis hacia la programación funcional, entre otras razones porque es intrínsecamente comprobable .

Cuanto más te mueves hacia el código SÓLIDO, más funcional se vuelve . Tarde o temprano, también puedes dar el paso. La arquitectura funcional es la arquitectura de Puertos y Adaptadores , y la inyección de dependencias también es un intento de Puertos y Adaptadores . La diferencia, sin embargo, es que un lenguaje como Haskell impone esa arquitectura a través de su sistema de tipos.

Favorecer la programación funcional de tipo estático

En este punto, esencialmente he renunciado a la programación orientada a objetos (OOP), aunque muchos de los problemas de OOP están intrínsecamente acoplados a lenguajes convencionales como Java y C # más que el concepto en sí.

El problema con los lenguajes OOP convencionales es que es casi imposible evitar el problema de la explosión combinatoria, que, sin probar, conduce a excepciones en tiempo de ejecución. Los lenguajes tipados estáticamente como Haskell y F #, por otro lado, le permiten codificar muchos puntos de decisión en el sistema de tipos. Esto significa que en lugar de tener que escribir miles de pruebas, el compilador simplemente le dirá si ha manejado todas las rutas de código posibles (hasta cierto punto; no es una bala de plata).

Además, la inyección de dependencia no es funcional . La verdadera programación funcional debe rechazar toda la noción de dependencias . El resultado es un código más simple.

Resumen

Si me veo obligado a trabajar con C #, prefiero la inyección de dependencia fina porque me permite cubrir todo el código base con una cantidad manejable de casos de prueba.

Al final, mi motivación es la retroalimentación rápida. Aún así, las pruebas unitarias no son la única forma de obtener comentarios .

Mark Seemann
fuente
1
Realmente me gusta esta respuesta, y especialmente esta declaración utilizada en el contexto de Spring: "Configurar dependencias usando XML es lo peor de ambos mundos. Primero, pierdes la seguridad de tipo de tiempo de compilación, pero no ganas nada. Una configuración XML el archivo puede ser tan grande como el código que intenta reemplazar ".
Thomas Carlisle
@ThomasCarlisle no era el punto de la configuración XML que no tienes que tocar el código (incluso compilar) para cambiarlo. Nunca (o apenas) lo usé debido a las dos cosas que Mark mencionó, pero a cambio ganas algo.
El Mac
1

Las enormes clases de configuración DI son un problema. Pero piense en el código que reemplaza.

Necesita crear una instancia de todos esos servicios y demás. El código para hacerlo estaría en el app.main()punto de partida e inyectado manualmente, o bien acoplado como this.myService = new MyService();dentro de las clases.

Reduzca el tamaño de su clase de configuración dividiéndola en varias clases de configuración y llámelas desde el punto de inicio del programa. es decir.

main()
{
   var c = new diContainer();
   var service1 = diSetupClass.SetupService1(c);
   var service2 = diSetupClass.SetupService2(c, service1); //if service1 is required by service2
   //etc

   //main logic
}

No necesita hacer referencia al servicio 1 o 2, excepto para pasarlos a otros métodos de configuración de ci.

Esto es mejor que intentar obtenerlos del contenedor, ya que impone el orden de llamar a las configuraciones.

Ewan
fuente
1
Para aquellos que desean profundizar en este concepto (construir su árbol de clases en el punto de inicio del programa), el mejor término para buscar es "raíz de composición"
e_i_pi
@Ewan, ¿qué es esa civariable?
superjos
su clase de configuración de ci con métodos estáticos
Ewan
urg debe ser di. sigo pensando 'contenedor'
Ewan