En mi proyecto de tamaño medio utilicé clases estáticas para repositorios, servicios, etc. y en realidad funcionó muy bien, incluso si la mayoría de los programadores esperan lo contrario. Mi código base era muy compacto, limpio y fácil de entender. Ahora intenté reescribir todo y usar IoC (Inversión de control) y estaba absolutamente decepcionado. Tengo que inicializar manualmente una docena de dependencias en cada clase, controlador, etc., agregar más proyectos para interfaces, etc. Realmente no veo ningún beneficio en mi proyecto y parece que causa más problemas de los que resuelve. Encontré los siguientes inconvenientes en IoC / DI:

  • tamaño de código mucho más grande
  • código de ravioli en lugar de código de espagueti
  • rendimiento más lento, es necesario inicializar todas las dependencias en el constructor incluso si el método al que quiero llamar solo tiene una dependencia
  • más difícil de entender cuando no se usa ningún IDE
  • algunos errores se envían al tiempo de ejecución
  • agregar dependencia adicional (marco DI en sí)
  • El personal nuevo tiene que aprender DI primero para trabajar con él.
  • una gran cantidad de código repetitivo, lo que es malo para las personas creativas (por ejemplo, copiar instancias del constructor a las propiedades ...)

No probamos todo el código base, sino solo ciertos métodos y usamos una base de datos real. Entonces, ¿debería evitarse la inyección de dependencia cuando no se requieren burlas para las pruebas?

13
Mark Smith 21 sep. 2016 a las 23:39

6 respuestas

La mejor respuesta

La mayoría de sus preocupaciones parecen reducirse a un mal uso o un malentendido.

  • tamaño de código mucho más grande

    Esto suele ser el resultado de respetar adecuadamente tanto el principio de responsabilidad única como el principio de segregación de interfaces. ¿Es drásticamente más grande? Sospecho que no es tan grande como dice. Sin embargo, lo más probable es que reduzca las clases a una funcionalidad específica, en lugar de tener clases "generales" que hacen cualquier cosa. En la mayoría de los casos, esto es un signo de separación saludable de preocupaciones, no un problema.

  • ravioli-code en lugar de spaghetti-code

    Una vez más, lo más probable es que esto haga que piense en pilas en lugar de en dependencias difíciles de ver. Creo que esto es un gran beneficio ya que conduce a una abstracción y encapsulación adecuadas.

  • rendimiento más lento Simplemente use un contenedor rápido. Mis favoritos son SimpleInjector y LightInject.

  • necesita inicializar todas las dependencias en el constructor incluso si el método al que quiero llamar tiene solo una dependencia

    Una vez más, esta es una señal de que está violando el principio de responsabilidad única. Esto es algo bueno porque te obliga a pensar lógicamente en tu arquitectura en lugar de agregar lo que quieras.

  • más difícil de entender cuando no se utiliza ningún IDE, algunos errores se envían al tiempo de ejecución

    Si TODAVÍA no está usando un IDE, la culpa es suya. No hay un buen argumento para ello con las máquinas modernas. Además, algunos contenedores (SimpleInjector) se validarán en la primera ejecución si así lo desea. Puede detectar esto fácilmente con una simple prueba unitaria.

  • agregar dependencia adicional (marco DI en sí mismo)

    Tienes que elegir tus batallas. Si el costo de aprender un nuevo marco es menor que el costo de mantener el código espagueti (y sospecho que lo será), entonces el costo está justificado.

  • el personal nuevo debe aprender DI primero para trabajar con él

    Si nos alejamos de los nuevos patrones, nunca crecemos. Pienso en esto como una oportunidad para enriquecer y hacer crecer a su equipo, no como una forma de lastimarlos. Además, la compensación es aprender el código espagueti, que puede ser mucho más difícil que seguir un patrón de toda la industria.

  • mucho código repetitivo que es malo para las personas creativas (por ejemplo, copiar instancias del constructor a las propiedades ...)

    Esto es completamente incorrecto. Las dependencias obligatorias siempre deben pasarse a través del constructor. Solo las dependencias opcionales deben establecerse a través de propiedades, y eso solo debe hacerse en circunstancias muy específicas, ya que a menudo se infringe el principio de responsabilidad única.

  • No probamos todo el código base, solo ciertos métodos y usamos una base de datos real. Entonces, ¿debería evitarse la inyección de dependencia cuando no se requieren burlas para las pruebas?

    Creo que este podría ser el mayor error de todos. La inyección de dependencia no es SOLO para facilitar las pruebas. Es para que pueda echar un vistazo a la firma de un constructor de clase y INMEDIATAMENTE saber lo que se requiere para que esa clase funcione. Esto es imposible con las clases estáticas ya que las clases pueden llamar tanto hacia arriba como hacia abajo de la pila cuando lo deseen sin ton ni son. Su objetivo debe ser agregar consistencia, claridad y distinción a su código. Esta es la razón más importante para usar DI y es por eso que le recomiendo que lo vuelva a visitar.

19
David L 21 sep. 2016 a las 21:11

Si tiene que inicializar las dependencias manualmente en el código, está haciendo algo mal. El patrón general para IoC es la inyección del constructor o, probablemente, la inyección de propiedades. La clase o el controlador no deberían saber nada sobre el contenedor DI.

Generalmente, todo lo que tienes que hacer es:

  1. configurar contenedor, como Interface = Class in Singleton scope
  2. Úselo, como Controller(Interface interface) {}
  3. Benefíciese de controlar todas las dependencias en un solo lugar

No veo ningún código repetitivo o un rendimiento más lento o cualquier otra cosa que hayas descrito. Realmente no puedo imaginarme cómo escribir una aplicación más o menos compleja sin ella.

Pero, en general, debe decidir qué es más importante. Para complacer a la "gente creativa" o crear una aplicación robusta y mantenible.

Por cierto, para crear una propiedad o archivar desde el constructor, puede usar Alt+Enter en R # y hace todo el trabajo por usted.

0
Traveler 21 sep. 2016 a las 20:48

Tenga cuidado: odio el IoC.

Aquí hay muchas respuestas excelentes que son reconfortantes. Los principales beneficios según Steven (respuesta muy fuerte) son:

  • Capacidad de prueba mejorada
  • Mayor flexibilidad
  • Mantenimiento mejorado
  • escalabilidad mejorada

Mis experiencias son muy diferentes a través, aquí están para un poco de equilibrio:

(Bonificación) Patrón de repositorio estúpido

Con demasiada frecuencia, esto se incluye junto con IoC. El patrón de repositorio solo debe usarse para acceder a datos externos y cuando la intercambiabilidad sea una expectativa principal.

Cuando usa esto, con Entity Framework, deshabilita todo el poder de Entity Framework, esto también sucede con las capas de servicio.

P.ej. Vocación:

var employees = peopleService.GetPeople(false, false, true, true); //Terrible

Debe ser:

var employees = db.People.ActiveOnly().ToViewModel();

En este caso usando métodos de extensión.

¿Quién necesita flexibilidad?

Si no tiene planes de cambiar las implementaciones de servicios, no lo necesita. Si cree que tendrá más de una implementación en el futuro, quizás agregue IoC entonces, y solo para esa parte.

¡Pero "probabilidad"!

Entity Framework (y probablemente otros ORM también), le permiten cambiar la cadena de conexión para apuntar a una base de datos en memoria. Por supuesto, eso solo está disponible a partir de EF7. Sin embargo, puede ser simplemente una base de datos de prueba nueva (adecuada) en un entorno de ensayo.

¿Tiene otros recursos de prueba y puntos de servicio especiales? En esta época, probablemente sean diferentes puntos finales URI de WebService, que también se pueden configurar en App.Config / Web.Config.

Las pruebas automatizadas hacen que su código se pueda mantener

TDD: si se trata de una aplicación web, utilice Jasmine o Selenium y realice pruebas de comportamiento automatizadas. Esto prueba todo hasta el usuario. Es una inversión a lo largo del tiempo, comenzando por cubrir características y funciones críticas.

DevOps / SysOps: mantenga scripts para aprovisionar todo su entorno (esta también es la mejor práctica), cree un entorno de prueba y ejecute todas las pruebas. También puede clonar su entorno de producción y ejecutar sus pruebas allí. No haga de "mantenible" y "comprobable" su excusa para elegir IoC. Comience con esos requisitos y encuentre las mejores formas de cumplirlos.

Escalabilidad: ¿de qué manera?

(Probablemente necesito leer el libro)

  • Para la escalabilidad del codificador, el control de versiones de código distribuido es la norma (aunque odio las fusiones).
  • Para la escalabilidad de los recursos humanos, no debería perder días diseñando capas abstractas adicionales para su proyecto.
  • Para la escalabilidad de producción de usuarios simultáneos, debe crear, probar y luego mejorar.
  • Para la escalabilidad del rendimiento del servidor, debe pensar en un nivel mucho más alto que IoC. ¿Va a ejecutar un servidor en la LAN del cliente? ¿Puedes replicar tus datos? ¿Está replicando a nivel de base de datos o de aplicación? ¿Es importante el acceso sin conexión en dispositivos móviles? Estas son preguntas sustanciales de arquitectura, donde IoC rara vez es la respuesta.

Prueba F12

Si está usando un IDE (lo que debería estar haciendo), como Visual Studio Community Edition, entonces sabrá lo útil que puede ser F12 para navegar por el código.

Con IoC, lo llevarán a la interfaz y luego deberá encontrar todas las referencias utilizando una interfaz en particular. Solo un paso más, pero para una herramienta que se usa tanto, me frustra.

Steven está en la pelota

Con Visual Studio 2015, simplemente puedo hacer CTRL + F12 para encontrar las implementaciones de una interfaz.

Sí, pero luego debe rastrear una lista de ambos usos, así como la declaración. (En realidad, creo que en el último VS, la declaración se enumera por separado, pero sigue siendo un clic adicional del mouse, quitando las manos del teclado. Y debo decir que esta es una limitación de Visual Studio, no puede llevarlo a un único implementación de la interfaz directamente.

2
Todd 18 dic. 2016 a las 01:47

¿Hubo alguna razón por la que no eligió utilizar una biblioteca IOC (StructureMap, Ninject, Autofac, etc.)? Usar cualquiera de estos le habría facilitado mucho la vida.

Aunque David L ya ha hecho un excelente conjunto de comentarios sobre sus puntos, agregaré los míos también.

Tamaño de código mucho más grande

No estoy seguro de cómo terminó con una base de código más grande; la configuración típica de una biblioteca IOC es bastante pequeña, y dado que está definiendo sus invariantes (dependencias) en los constructores de clases, también está eliminando algo de código (es decir, el "nuevo xyz ()" cosas) que no necesita Más.

Código ravioli en lugar de código espagueti

Sucede que me gustan bastante los ravioles :)

Rendimiento más lento, es necesario inicializar todas las dependencias en el constructor incluso si el método al que quiero llamar solo tiene una dependencia

Si está haciendo esto, entonces realmente no está usando Dependency Injection en absoluto. Debería recibir gráficos de objetos completamente cargados y listos para usar a través de los argumentos de dependencia declarados en los parámetros del constructor de la clase en sí, ¡no crearlos en el constructor! La mayoría de las bibliotecas IOC modernas son ridículamente rápidas y nunca serán un problema de rendimiento. Aquí un buen video que demuestra el punto.

Más difícil de entender cuando no se usa ningún IDE

Eso es cierto, pero también significa que puedes aprovechar la oportunidad para pensar en términos de abstracciones. Entonces, por ejemplo, puedes mirar un fragmento de código

public class Something
{
    readonly IFrobber _frobber;
    public Something(IFrobber frobber)
    {
        _frobber=frobber;
    }
    
    public void LetsFrobSomething(Thing theThing)
    {
        _frobber.Frob(theThing)
    }
}

Cuando mira este código y trata de averiguar si funciona, o si es la causa raíz de un problema, puede ignorar la implementación real de IFrobber; simplemente representa la capacidad abstracta de Frobber algo, y no necesitas llevar mentalmente cómo un Frobber en particular podría hacer su trabajo. puede concentrarse en asegurarse de que esta clase haga lo que se supone que debe hacer, es decir, delegar algo de trabajo a un Frobber de algún tipo.

Tenga en cuenta también que ni siquiera necesita utilizar interfaces aquí; también puede seguir adelante e inyectar implementaciones concretas. Sin embargo, eso tiende a violar el principio de inversión de dependencia (que solo está relacionado tangenitalmente con el DI del que estamos hablando aquí) porque obliga a la clase a depender de una concreción en lugar de una abstracción.

Algunos errores se envían al tiempo de ejecución

Ni más ni menos de lo que serían con la construcción manual de gráficos en el constructor;

Agregar dependencia adicional (marco DI en sí)

Eso también es cierto, pero la mayoría de las bibliotecas de IOC son bastante pequeñas y discretas, y en algún momento tienes que decidir si la compensación de tener un artefacto de producción un poco más grande vale la pena (realmente lo es)

El personal nuevo debe aprender DI primero para trabajar con él

Eso no es realmente diferente de lo que sería el caso con cualquier nueva tecnología :) Aprender a usar una biblioteca IOC tiende a abrir la mente a otras posibilidades como TDD, los principios SOLID, etc., ¡lo cual nunca es algo malo!

Mucho código repetitivo, lo cual es malo para las personas creativas (por ejemplo, copiar instancias del constructor a las propiedades ...)

No entiendo este, cómo puede terminar con mucho código repetitivo; No consideraría almacenar las dependencias dadas en miembros privados de solo lectura como un texto estándar de lo que vale la pena hablar, teniendo en cuenta que si tiene más de 3 o 4 dependencias por clase, es probable que esté violando el SRP y debería reconsiderar su diseño.

Finalmente, si no está convencido por ninguno de los argumentos presentados aquí, le recomendaría leer "Inyección de dependencias en .Net". (o cualquier otra cosa que tenga que decir sobre DI que puede encontrar en su blog). Le prometo que aprenderá algunas cosas útiles y puedo decirle que cambió mi forma de escribir software para mejor.

1
Community 20 jun. 2020 a las 09:12

Hay muchos argumentos de 'libros de texto' a favor del uso de IoC, pero en mi experiencia personal, las ganancias son / fueron:

Posibilidad de probar solo partes del proyecto y simular otras partes . Por ejemplo, si tiene un componente que devuelve la configuración de la base de datos, es fácil simularlo para que su prueba pueda funcionar sin una base de datos real. Con clases estáticas esto no es posible.

Mejor visibilidad y control de las dependencias . Con las clases estáticas es muy fácil agregar algunas dependencias sin siquiera darse cuenta, que pueden crear problemas más adelante. Con IoC, esto es más explícito y visible.

Orden de inicialización más explícito . Con las clases estáticas, esto puede ser a menudo una caja negra y puede haber problemas latentes debido al uso circular.

El único inconveniente para mí fue que al colocar todo antes de las interfaces no es posible navegar directamente a la implementación desde el uso (F12).

Sin embargo, son los desarrolladores de un proyecto quienes pueden juzgar mejor los pros y los contras en el caso particular.

1
Adam Rotaru 21 sep. 2016 a las 21:02

Aunque IoC / DI no es una solución milagrosa que funcione en todos los casos, es posible que no lo haya aplicado correctamente. Toma tiempo dominar el conjunto de principios detrás de la inyección de dependencia, o al menos, seguro que lo hizo para mí. Cuando se aplica correctamente, puede traer (entre otros) los siguientes beneficios:

  • Capacidad de prueba mejorada
  • Flexibilidad mejorada
  • Mantenibilidad mejorada
  • Desarrollo paralelo mejorado

De su pregunta, ya puedo extraer algunas cosas que podrían haber salido mal en su caso:

Tengo que inicializar manualmente una docena de dependencias en cada clase

Esto implica que cada clase que cree es responsable de crear las dependencias que requiere. Este es un anti-patrón conocido como Control Freak . Una clase no debería new aumentar sus dependencias por sí misma. Incluso podría haber aplicado el antipatrón del localizador de servicios donde su La clase solicita sus dependencias llamando al contenedor (o una abstracción que representa el contenedor) para obtener una dependencia particular. Una clase debería simplemente definir las dependencias que requiere como argumentos de constructor.

docena de dependencias

Esta declaración implica que está infringiendo el principio de responsabilidad única. En realidad, esto no está acoplado a IoC / DI, su código anterior probablemente ya violó el principio de responsabilidad única, lo que hace que sea difícil de entender y mantener para otros desarrolladores. A menudo es difícil para el autor original entender por qué otros tienen dificultades para mantener el código, ya que lo que escribiste a menudo encaja perfectamente en tu cabeza. A menudo, la violación del SRP hará que otros tengan problemas para comprender y mantener el código. Y probar clases que violan SRP a menudo es aún más difícil. Una clase debe tener media docena de dependencias como máximo.

agregar más proyectos para interfaces y así sucesivamente

Esto implica que está infringiendo el principio de abstracción reutilizado. En general, la mayoría de los componentes / clases de su aplicación deben estar cubiertos por una docena de abstracciones. Por ejemplo, todas las clases que implementan algún caso de uso probablemente merecen un solo abstracción (genérica). Las clases que implementan consultas también merecen una abstracción. Para los sistemas que escribo, del 80% al 95% de mis componentes (clases que contienen el comportamiento de la aplicación) están cubiertos por 5 a 12 abstracciones (en su mayoría genéricas). La mayoría de las veces no es necesario crear un nuevo proyecto únicamente para las interfaces. La mayoría de las veces coloco esas interfaces en la raíz del mismo proyecto.

tamaño de código mucho más grande

La cantidad de código que escriba inicialmente no será muy diferente. Sin embargo, la práctica de la inyección de dependencia solo funciona muy bien cuando se aplica SOLID también , y SOLID promueve clases pequeñas y enfocadas. Clases con una sola responsabilidad. Esto significa que tendrá muchas clases pequeñas que son fáciles de entender y fáciles de componer en sistemas flexibles. Y no lo olvide: no deberíamos esforzarnos por escribir menos código, sino más código mantenible.

Sin embargo, con un buen diseño SÓLIDO y las abstracciones adecuadas en su lugar, experimenté tener que escribir mucho menos código del que tenía antes. Por ejemplo, la aplicación de ciertas preocupaciones transversales (como registro, seguimiento de auditoría, autorización, etc.) se puede aplicar simplemente escribiendo unas pocas líneas de código en la capa de infraestructura de la aplicación, en lugar de tener que esparcirlo por todo el solicitud. Incluso me llevó a poder hacer cosas que antes no eran factibles, porque me obligaron a realizar cambios radicales en todo el código base, lo que consumía tanto tiempo que la administración no me permitía hacerlo.

ravioli-code en lugar de spaghetti-code es más difícil de entender cuando no se usa IDE

Esto es cierto. La inyección de dependencia promueve que las clases se desacoplen unas de otras. Esto a veces puede dificultar la búsqueda de una base de código, ya que una clase generalmente depende de una abstracción en lugar de clases concretas. En el pasado, encontré que la flexibilidad que me brinda DI supera con creces el costo de encontrar la implementación. Con Visual Studio 2015, simplemente puedo hacer CTRL + F12 para encontrar las implementaciones de una interfaz. Si solo hay una implementación, Visual Studio irá directamente a esa implementación.

rendimiento más lento

Esto no es verdad. El rendimiento no tiene por qué ser diferente a trabajar con un código base de solo llamadas a métodos estáticos. Sin embargo, eligió tener sus clases con un estilo de vida transitorio, lo que significa que tiene nuevas instancias por todas partes. En mis últimas aplicaciones, creé todas mis clases solo una vez por aplicación, lo que proporciona aproximadamente el mismo rendimiento que tener solo llamadas a métodos estáticos, pero con el beneficio de que la aplicación es muy flexible y fácil de mantener. Pero tenga en cuenta que incluso si decide new completar gráficos de objetos para cada solicitud (web), es muy probable que el costo de rendimiento sea un orden de magnitud menor que cualquier E / S (bases de datos, sistema de archivos y llamadas a servicios web) que realiza durante esa solicitud, incluso con los contenedores DI más lentos.

algunos errores se envían al tiempo de ejecución agregando dependencia adicional (marco DI en sí)

Ambos problemas implican el uso de una biblioteca DI. Las bibliotecas DI realizan la composición de objetos en tiempo de ejecución. Sin embargo, una biblioteca DI no es una herramienta necesaria para practicar la inyección de dependencia. Las aplicaciones pequeñas pueden beneficiarse del uso de Dependency Injection sin una herramienta; una práctica llamada Pure DI. Es posible que su aplicación no se beneficie del uso de un contenedor DI, pero la mayoría de las aplicaciones realmente se benefician del uso de la inyección de dependencia (cuando se usa correctamente) como práctica. De nuevo: las herramientas son opcionales, la escritura de código mantenible no lo es.

Pero incluso si usa una biblioteca DI, hay bibliotecas que tienen herramientas integradas que le permiten verificar y diagnosticar su configuración. No le brindarán soporte en tiempo de compilación, pero le permitirán ejecutar este análisis cuando se inicia la aplicación o mediante una prueba unitaria. Esto le impide hacer una regresión en la aplicación completa solo para verificar si su contenedor está cableado correctamente. Mi consejo es que elija un contenedor DI que le ayude a detectar estos errores de configuración.

El personal nuevo tiene que aprender DI primero para trabajar con él.

Esto es cierto, pero la inyección de dependencia en sí no es realmente difícil de aprender. Lo que realmente es difícil de aprender es aplicar los principios SOLID correctamente, y debe aprender esto de todos modos cuando desee escribir aplicaciones que deben ser mantenidas por más de un desarrollador durante un período de tiempo considerado. Prefiero invertir en enseñar a los desarrolladores de mi equipo a escribir código SÓLIDO en lugar de simplemente dejarles generar código; eso seguramente causará un infierno de mantenimiento más adelante.

mucho código repetitivo

Hay algo de código repetitivo cuando miramos el código escrito en C # 6, pero en realidad no es tan malo, especialmente si se consideran las ventajas que ofrece. Y las versiones futuras de C # eliminarán el texto estándar que se debe principalmente a tener que definir constructores que toman argumentos que se verifican con nulos y se asignan a variables privadas. C # 7 u 8 seguramente solucionará esto cuando se introduzcan tipos de registro y tipos de referencia que no aceptan valores NULL.

que es malo para las personas creativas

Lo siento, pero este argumento es pura tontería. He visto este argumento utilizado una y otra vez como una excusa para escribir código incorrecto por desarrolladores que no querían aprender sobre patrones de diseño y principios y prácticas de software. Ser creativo no es excusa para escribir código que nadie más puede entender o código que es imposible de probar. Necesitamos aplicar patrones y prácticas aceptadas y dentro de ese límite hay suficiente espacio para ser creativos mientras escribimos un buen código. Escribir código no es un arte; es un arte.

Como dije, DI no es apropiado en todos los casos, y las prácticas que lo rodean requieren tiempo para dominarlas. Puedo aconsejarle que lea el libro Inyección de dependencia en .NET de Mark Seemann; le dará muchas respuestas y le dará una buena idea de cómo y cuándo aplicarlo y cuándo no.

10
Steven 17 dic. 2016 a las 20:58