¿Cuál es la forma "correcta" de implementar DI en .NET?

22

Estoy buscando implementar la inyección de dependencia en una aplicación relativamente grande, pero no tengo experiencia en ello. Estudié el concepto y algunas implementaciones de IoC e inyectores de dependencia disponibles, como Unity y Ninject. Sin embargo, hay una cosa que me está eludiendo. ¿Cómo debo organizar la creación de instancias en mi aplicación?

Lo que estoy pensando es que puedo crear algunas fábricas específicas que contendrán la lógica de crear objetos para algunos tipos de clase específicos. Básicamente, una clase estática con un método que invoca el método Ninject Get () de una instancia de kernel estático en esta clase.

¿Será un enfoque correcto para implementar la inyección de dependencia en mi aplicación o debería implementarlo de acuerdo con algún otro principio?

usuario3223738
fuente
55
No creo que haya la manera correcta, sino muchas formas correctas, dependiendo de su proyecto. Me adheriría a los demás y sugeriría la inyección del constructor, ya que podrá asegurarse de que cada dependencia se inyecte en un solo punto. Además, si las firmas del constructor crecen demasiado, sabrá que las clases hacen demasiado.
Paul Kertscher
Es difícil responder sin saber qué tipo de proyecto .net está creando. Una buena respuesta para WPF podría ser una mala respuesta para MVC, por ejemplo.
JMK
Es bueno organizar todos los registros de dependencias en un módulo DI para la solución o para cada proyecto, y posiblemente uno para algunas pruebas dependiendo de lo que desee probar. Ah sí, por supuesto, deberías usar la inyección de constructor, lo otro es para usos más avanzados / locos.
Mark Rogers

Respuestas:

30

No pienses aún en la herramienta que vas a utilizar. Puede hacer DI sin un contenedor de IoC.

Primer punto: Mark Seemann tiene un muy buen libro sobre DI en .Net

Segundo: composición raíz. Asegúrese de que toda la configuración se realice en el punto de entrada del proyecto. El resto de su código debe saber sobre las inyecciones, no sobre cualquier herramienta que se esté utilizando.

Tercero: la inyección de constructor es el camino más probable (hay casos en los que no lo querría, pero no tantos).

Cuarto: considere el uso de fábricas lambda y otras características similares para evitar crear interfaces / clases innecesarias con el único propósito de inyección.

Miyamoto Akira
fuente
55
Todos excelentes consejos; especialmente la primera parte: aprenda a hacer DI puro y luego comience a buscar contenedores de IoC que puedan reducir la cantidad de código repetitivo requerido por ese enfoque.
David Arno
66
... u omita todos los contenedores de IoC para mantener todos los beneficios de la verificación estática.
Den
Me gusta que este consejo sea un buen punto de partida, antes de entrar en DI, y realmente tengo el libro de Mark Seemann.
Snoop
Puedo secundar esta respuesta. Usamos con éxito pobre-mans-DI (bootstrapper escrito a mano) en una aplicación bastante grande rompiendo partes de la lógica de bootstrapper en los módulos que componían la aplicación.
wigy
1
Ten cuidado. El uso de inyecciones lambda puede ser un camino rápido hacia la locura, especialmente del tipo de daño de diseño inducido por la prueba. Lo sé. He seguido ese camino.
jpmc26
13

Su pregunta tiene dos partes: cómo implementar DI correctamente y cómo refactorizar una aplicación grande para usar DI.

La primera parte es bien respondida por @Miyamoto Akira (especialmente la recomendación de leer el libro "inyección de dependencia en .net" de Mark Seemann. El blog de Marcas también es un buen recurso gratuito.

La segunda parte es mucho más complicada.

Un buen primer paso sería simplemente mover todas las instancias a los constructores de clases, sin inyectar las dependencias, solo asegurándose de que solo llame newal constructor.

Esto resaltará todas las violaciones de SRP que ha estado cometiendo, para que pueda comenzar a dividir la clase en colaboradores más pequeños.

El próximo problema que encontrará serán las clases que dependen de parámetros de tiempo de ejecución para la construcción. Por lo general, puede solucionar esto creando fábricas simples, a menudo con Func<param,type>, inicializándolas en el constructor y llamándolas en los métodos.

El siguiente paso sería crear interfaces para sus dependencias y agregar un segundo constructor a sus clases que excepto estas interfaces. Su constructor sin parámetros renovaría las instancias concretas y las pasaría al nuevo constructor. Esto se conoce comúnmente como 'Inyección B * stard' o 'Pobre mans DI'.

Esto le dará la capacidad de hacer algunas pruebas unitarias, y si ese era el objetivo principal del refactor, puede ser donde se detenga. El nuevo código se escribirá con la inyección del constructor, pero su código antiguo puede continuar funcionando como está escrito pero aún así se puede probar.

Por supuesto, puedes ir más allá. Si tiene la intención de usar un contenedor IOC, entonces el siguiente paso podría ser reemplazar todas las llamadas directas newen sus constructores sin parámetros con llamadas estáticas al contenedor IOC, esencialmente (ab) usándolo como un localizador de servicios.

Esto arrojará más casos de parámetros de constructor de tiempo de ejecución para tratar como antes.

Una vez hecho esto, puede comenzar a eliminar los constructores sin parámetros y refactorizar a DI puro.

En última instancia, esto va a ser mucho trabajo, así que asegúrese de decidir por qué quiere hacerlo y priorice las partes de la base de código que se beneficiarán más del refactorizador.

Steve
fuente
3
Gracias por una respuesta muy elaborada. Me diste algunas ideas sobre cómo abordar el problema que estoy enfrentando. Toda la arquitectura de la aplicación ya está construida con IoC en mente. La razón principal por la que quiero usar DI no es ni siquiera una prueba unitaria, viene como una ventaja, sino más bien una capacidad para intercambiar diferentes implementaciones por diferentes interfaces, definidas en el núcleo de mi aplicación, con el menor esfuerzo posible. La aplicación en cuestión funciona en un entorno en constante cambio y, a menudo, tengo que intercambiar partes de la aplicación para usar nuevas implementaciones de acuerdo con los cambios en el entorno.
user3223738
1
Me alegro de poder ayudar, y sí, estoy de acuerdo en que la mayor ventaja de DI es el acoplamiento flojo y la capacidad de reconfigurar fácilmente que esto trae, con las pruebas unitarias como un buen efecto secundario.
Steve
1

Primero, quiero mencionar que está haciendo esto mucho más difícil para usted al refactorizar un proyecto existente en lugar de comenzar un nuevo proyecto.

Dijiste que es una aplicación grande, así que elige un componente pequeño para comenzar. Preferiblemente un componente 'nodo de hoja' que no es usado por nada más. No sé cuál es el estado de las pruebas automatizadas en esta aplicación, pero interrumpirá todas las pruebas unitarias para este componente. Así que esté preparado para eso. El paso 0 es escribir pruebas de integración para el componente que modificará si aún no existen. Como último recurso (sin infraestructura de prueba; sin aceptación para escribirlo), descubra una serie de pruebas manuales que puede hacer para verificar que este componente esté funcionando.

La forma más sencilla de establecer su objetivo para el refactor DI es que desea eliminar todas las instancias del operador 'nuevo' de este componente. Estos generalmente se dividen en dos categorías:

  1. Variable miembro invariable: son variables que se establecen una vez (generalmente en el constructor) y no se reasignan durante la vida útil del objeto. Para estos, puede inyectar una instancia del objeto en el constructor. Por lo general, no es responsable de deshacerse de estos objetos (no quiero decir que nunca aquí, pero realmente no debería tener esa responsabilidad).

  2. Variable miembro variable / variable de método: Estas son variables que obtendrán basura recolectada en algún momento durante la vida útil del objeto. Para estos, querrá inyectar una fábrica en su clase para proporcionar estas instancias. Usted es responsable de eliminar los objetos creados por una fábrica.

Su contenedor IoC (ninject suena como) se encargará de crear instancias de esos objetos e implementar sus interfaces de fábrica. Lo que sea que esté usando el componente que ha modificado necesitará saber sobre el contenedor IoC para que pueda recuperar su componente.

Una vez que haya completado lo anterior, podrá obtener los beneficios que espera obtener de DI en su componente seleccionado. Ahora sería un buen momento para agregar / corregir esas pruebas unitarias. Si hubiera pruebas unitarias existentes, tendrá que tomar una decisión sobre si desea unirlas mediante la inyección de objetos reales o escribir nuevas pruebas unitarias utilizando simulacros.

'Simplemente' repita lo anterior para cada componente de su aplicación, moviendo la referencia al contenedor de IoC hacia arriba a medida que avanza hasta que solo sea necesario saberlo.

Jon
fuente
1
Un buen consejo: comience con un componente pequeño en lugar de la 'GRAN reescritura'
Daniel Hollinrake
0

El enfoque correcto es usar inyección de constructor, si usa

Lo que estoy pensando es que puedo crear algunas fábricas específicas que contendrán la lógica de crear objetos para algunos tipos de clase específicos. Básicamente, una clase estática con un método que invoca el método Ninject Get () de una instancia de kernel estático en esta clase.

entonces terminas con el localizador de servicios, que la inyección de dependencia.

Pelican de bajo vuelo
fuente
Seguro. Inyector de constructor. Digamos que tengo una clase que acepta una implementación de interfaz como uno de los argumentos. Pero todavía necesito crear una instancia de la implementación de la interfaz y pasarla a este constructor en alguna parte. Preferiblemente debería ser una pieza centralizada de código.
user3223738
2
No, cuando inicializa el contenedor DI, debe especificar la implementación de las interfaces, y el contenedor DI creará la instancia e inyectará al constructor.
Low Flying Pelican
Personalmente, encuentro que la inyección de constructor se usa en exceso. He visto con demasiada frecuencia que se inyectan 10 servicios diferentes y que solo se necesita uno para una llamada de función, ¿por qué no es esa parte del argumento de la función?
urbanhusky
2
Si se inyectan 10 servicios diferentes, es porque alguien está violando SRP, que debería dividirse en componentes más pequeños.
Low Flying Pelican
1
@Fabio La pregunta es qué te compraría eso. Todavía tengo que ver un ejemplo en el que tener una clase gigante que se ocupe de una docena de cosas totalmente diferentes es un buen diseño. Lo único que hace DI es hacer que todas esas violaciones sean más obvias.
Voo
0

Dices que quieres usarlo pero no dices por qué.

DI no es más que proporcionar un mecanismo para generar concreciones a partir de interfaces.

Esto en sí mismo proviene del DIP . Si su código ya está escrito en este estilo y tiene un solo lugar donde se generan concreciones, DI no trae nada más a la fiesta. Agregar código de marco DI aquí simplemente hincharía y ofuscaría su base de código.

Asumiendo que no desee utilizarlo, normalmente se configura la fábrica / constructor / contenedor (o lo que sea) al comienzo de la aplicación por lo que es claramente visible.

Nota: es muy fácil rodar el suyo si lo desea, en lugar de comprometerse con Ninject / StructureMap o lo que sea. Sin embargo, si tiene una rotación de personal razonable, puede engrasar las ruedas para usar un marco reconocido o al menos escribirlo con ese estilo para que no sea una curva de aprendizaje demasiado.

Robbie Dee
fuente
0

En realidad, la forma "correcta" es NO usar una fábrica en absoluto a menos que no haya absolutamente otra opción (como en las pruebas unitarias y ciertos simulacros, ¡para el código de producción NO se usa una fábrica)! Hacerlo es en realidad un antipatrón y debe evitarse a toda costa. El objetivo detrás de un contenedor DI es permitir que el dispositivo haga el trabajo por usted.

Como se indicó anteriormente en una publicación anterior, desea que su dispositivo IoC asuma la responsabilidad de la creación de los diversos objetos dependientes en su aplicación. Eso significa permitir que su dispositivo DI cree y administre las distintas instancias. Este es todo el punto detrás de DI: sus objetos NUNCA deben saber cómo crear y / o administrar los objetos de los que dependen. Hacer lo contrario rompe el acoplamiento flojo.

La conversión de una aplicación existente a todos los DI es un gran paso, pero dejando de lado las dificultades obvias para hacerlo, también querrá (solo para hacer su vida un poco más fácil) explorar una herramienta DI que realizará la mayor parte de sus enlaces automáticamente (el núcleo de algo como Ninject son las "kernel.Bind<someInterface>().To<someConcreteClass>()"llamadas que realiza para hacer coincidir sus declaraciones de interfaz con las clases concretas que desea utilizar para implementar esas interfaces. Son esas llamadas "Bind" las que permiten que su dispositivo DI intercepte sus llamadas de constructor y proporcione instancias de objeto dependiente necesarias Un constructor típico (pseudocódigo mostrado aquí) para alguna clase podría ser:

public class SomeClass
{
  private ISomeClassA _ClassA;
  private ISomeOtherClassB _ClassB;

  public SomeClass(ISomeClassA aInstanceOfA, ISomeOtherClassB aInstanceOfB)
  {
    if (aInstanceOfA == null)
      throw new NullArgumentException();
    if (aInstanceOfB == null)
      throw new NullArgumentException();
    _ClassA = aInstanceOfA;
    _ClassB = aInstanceOfB;
  }

  public void DoSomething()
  {
    _ClassA.PerformSomeAction();
    _ClassB.PerformSomeOtherActionUsingTheInstanceOfClassA(_ClassA);
  }
}

Tenga en cuenta que en ningún lugar de ese código había ningún código que creara / administrara / liberara la instancia de SomeConcreteClassA o SomeOtherConcreteClassB. De hecho, ninguna clase concreta fue referenciada. Entonces ... ¿dónde sucedió la magia?

En la parte de inicio de su aplicación, ocurrió lo siguiente (de nuevo, este es un pseudocódigo pero está bastante cerca de lo real (Ninject) ...):

public void StartUp()
{
  kernel.Bind<ISomeClassA>().To<SomeConcreteClassA>();
  kernel.Bind<ISomeOtherClassB>().To<SomeOtherConcreteClassB>();
}

Ese pequeño fragmento de código le dice al dispositivo Ninject que busque constructores, los escanee, busque instancias de interfaces que haya sido configurado para manejar (esas son las llamadas "Bind") y luego cree y sustituya una instancia de la clase concreta donde sea Se hace referencia a la instancia.

Hay una buena herramienta que complementa muy bien a Ninject llamada Ninject.Extensions.Conventions (otro paquete NuGet) que hará la mayor parte de este trabajo por usted. No para alejarse de la excelente experiencia de aprendizaje que atravesará a medida que lo desarrolle usted mismo, pero para comenzar, esta podría ser una herramienta para investigar.

Si la memoria funciona, Unity (formalmente de Microsoft ahora un proyecto de código abierto) tiene una llamada al método o dos que hacen lo mismo, otras herramientas tienen ayudantes similares.

Sea cual sea el camino que elija, definitivamente lea el libro de Mark Seemann para la mayor parte de su formación DI, sin embargo, debe señalarse que incluso los "Grandes" del mundo de la ingeniería de software (como Mark) pueden cometer errores evidentes: Mark se olvidó por completo de Ninject en su libro, así que aquí hay otro recurso escrito solo para Ninject. Lo tengo y es una buena lectura: Dominar Ninject para inyección de dependencia

Fred
fuente
0

No existe un "camino correcto", pero hay algunos principios simples a seguir:

  • Crear la raíz de composición en el inicio de la aplicación
  • Después de que se haya creado la raíz de la composición, deseche la referencia al contenedor / núcleo DI (o al menos encapsúlela para que no sea directamente accesible desde su aplicación)
  • No cree instancias a través de "nuevo"
  • Pase todas las dependencias requeridas como abstracción al constructor

Eso es todo. Por supuesto, esos son principios, no leyes, pero si los sigue, puede estar seguro de que hace DI (corríjame si me equivoco).


Entonces, ¿cómo crear objetos durante el tiempo de ejecución sin "nuevo" y sin conocer el contenedor DI?

En el caso de NInject, hay una extensión de fábrica que proporciona la creación de fábricas. Claro, las fábricas creadas todavía tienen una referencia interna al núcleo, pero eso no es accesible desde su aplicación.

JanDotNet
fuente