¿Cómo evitar la locura del constructor de inyección de dependencia?

300

Me parece que mis constructores comienzan a verse así:

public MyClass(Container con, SomeClass1 obj1, SomeClass2, obj2.... )

con una lista de parámetros cada vez mayor. Dado que "Contenedor" es mi contenedor de inyección de dependencia, ¿por qué no puedo hacer esto?

public MyClass(Container con)

para cada clase? ¿Cuáles son las desventajas? Si hago esto, parece que estoy usando una estática glorificada. Por favor comparta sus pensamientos sobre IoC y la locura de la inyección de dependencia.

JP Richardson
fuente
64
¿Por qué pasas el contenedor? Creo que puede malinterpretar el COI
Paul Creasey
33
Si sus constructores exigen más o más parámetros, puede estar haciendo demasiado en esas clases.
Austin Salonen
38
Así no se hace la inyección de constructor. Los objetos no saben nada sobre el contenedor IoC, ni deberían saberlo.
duffymo
Simplemente puede crear un constructor vacío, en el que llame al DI directamente y le pregunte por lo que necesita. Eso eliminará el constructor madnes, pero debe asegurarse de que está utilizando una interfaz DI ... en caso de que cambie su sistema DI a mitad del desarrollo. Honestamente ... nadie volverá a hacerlo de esta manera, aunque esto es lo que DI hace para inyectar en su constructor. doh
Piotr Kula

Respuestas:

409

Tiene razón en que si usa el contenedor como un Localizador de servicios, es más o menos una fábrica estática glorificada. Por muchas razones , considero que esto es un antipatrón .

Uno de los maravillosos beneficios de la inyección de constructor es que hace que las violaciones del principio de responsabilidad única sean evidentemente evidentes.

Cuando eso sucede, es hora de refactorizar a Facade Services . En resumen, cree una nueva interfaz más detallada que oculte la interacción entre algunas o todas las dependencias específicas que necesita actualmente.

Mark Seemann
fuente
8
+1 para cuantificar el esfuerzo de refactorización en un solo concepto; impresionante :)
Ryan Emerle
46
¿de verdad? acabas de crear una indirección para mover esos parámetros a otra clase, ¡pero todavía están allí! simplemente más complicado tratar con ellos.
irreputable el
23
@irreputable: en el caso degenerado en el que trasladamos todas las dependencias a un Servicio agregado, estoy de acuerdo en que es solo otro nivel de indirección que no tiene ningún beneficio, por lo que mi elección de palabras fue ligeramente diferente. Sin embargo, el punto es que trasladamos solo algunas de las dependencias detalladas a un Servicio Agregado. Esto limita el número de permutaciones de dependencias tanto en el nuevo Servicio agregado como para las dependencias dejadas atrás. Esto hace que ambos sean más fáciles de manejar.
Mark Seemann
92
La mejor observación: "Uno de los maravillosos beneficios de la inyección de constructor es que hace que las violaciones del principio de responsabilidad única sean evidentemente evidentes".
Igor Popov
2
@DonBox En ese caso, puede escribir implementaciones de objetos nulos para detener la recursividad. No es lo que necesita, pero el punto es que la inyección de constructor no previene los ciclos, solo deja en claro que están allí.
Mark Seemann
66

No creo que los constructores de tu clase deban tener una referencia a tu período de contenedor IOC. Esto representa una dependencia innecesaria entre su clase y el contenedor (¡el tipo de dependencia que IOC está tratando de evitar!).

derivación
fuente
+1 Dependiendo de un contenedor de IoC hace que sea difícil cambiar ese contenedor más adelante sin cambiar el montón de código en todas las demás clases
Tseng
1
¿Cómo implementaría IOC sin tener parámetros de interfaz en los constructores? ¿Estoy leyendo mal tu publicación?
J Hunt
@J Hunt No entiendo tu comentario. Para mí, los parámetros de interfaz significan parámetros que son interfaces para las dependencias, es decir, si su contenedor de inyección de dependencia se inicializa MyClass myClass = new MyClass(IDependency1 interface1, IDependency2 interface2)(parámetros de interfaz). Esto no está relacionado con posterior @ de derivación, que interpreto como diciendo un contenedor de inyección de dependencias no debe inyectarse en sus objetos, es decir,MyClass myClass = new MyClass(this)
John Doe
25

La dificultad de pasar los parámetros no es el problema. El problema es que su clase está haciendo demasiado y debería desglosarse más.

La inyección de dependencia puede actuar como una advertencia temprana para las clases que se hacen demasiado grandes, específicamente debido al creciente dolor de pasar todas las dependencias.

Kyoryu
fuente
44
Corrígeme si estoy equivocado, pero en algún momento tienes que 'pegar todo junto', y por lo tanto tienes que obtener más de unas pocas dependencias para eso. Por ejemplo, en la capa Vista, al crear plantillas y datos para ellos, debe tomar todos los datos de varias dependencias (por ejemplo, 'servicios') y luego poner todos estos datos en la plantilla y en la pantalla. Si mi página web tiene 10 'bloques' diferentes de información, entonces necesito 10 clases diferentes para proporcionarme esos datos. ¿Entonces necesito 10 dependencias en mi clase Ver / Plantilla?
Andrew
4

Me encontré con una pregunta similar sobre la inyección de dependencia basada en el constructor y lo complejo que era pasar en todas las dependencias.

Uno de los enfoques que he usado en el pasado es usar el patrón de fachada de la aplicación usando una capa de servicio. Esto tendría una API gruesa. Si este servicio depende de repositorios, usaría una inyección de setter de las propiedades privadas. Esto requiere crear una fábrica abstracta y mover la lógica de crear los repositorios a una fábrica.

Código detallado con explicación se puede encontrar aquí

Mejores prácticas para IoC en la capa de servicios complejos

samsur
fuente
3

Leí todo este hilo, dos veces, y creo que las personas responden por lo que saben, no por lo que se les pregunta.

La pregunta original de JP parece que está construyendo objetos enviando un resolutor, y luego un montón de clases, pero estamos asumiendo que esas clases / objetos son en sí mismos servicios, listos para la inyección. ¿Y si no lo son?

JP, si está buscando aprovechar la DI y desea la gloria de mezclar la inyección con datos contextuales, ninguno de estos patrones (o supuestos "antipatrones") aborda específicamente eso. En realidad, se reduce a usar un paquete que lo apoyará en tal esfuerzo.

Container.GetSevice<MyClass>(someObject1, someObject2)

... este formato rara vez es compatible. Creo que la dificultad de programar dicho soporte, sumado al rendimiento miserable que se asociaría con la implementación, lo hace poco atractivo para los desarrolladores de código abierto.

Pero debería hacerse, porque debería ser capaz de crear y registrar una fábrica para MyClass'es, y esa fábrica debería poder recibir datos / entradas que no se conviertan en un "servicio" por el simple hecho de pasar datos. Si el "antipatrón" se trata de consecuencias negativas, entonces forzar la existencia de tipos de servicios artificiales para pasar datos / modelos es ciertamente negativo (a la par con su sensación de envolver sus clases en un contenedor. Se aplica el mismo instinto).

Sin embargo, hay un marco que puede ayudar, incluso si se ven un poco feos. Por ejemplo, Ninject:

Crear una instancia usando Ninject con parámetros adicionales en el constructor

Eso es para .NET, es popular y todavía no está tan limpio como debería ser, pero estoy seguro de que hay algo en el idioma que elijas emplear.

Craig Brunetti
fuente
3

Inyectar el contenedor es un atajo que finalmente lamentará.

La inyección excesiva no es el problema, generalmente es un síntoma de otros defectos estructurales, especialmente la separación de preocupaciones. Este no es un problema, pero puede tener muchas fuentes y lo que hace que esto sea tan difícil de solucionar es que tendrá que lidiar con todos ellos, a veces al mismo tiempo (piense en desenredar espaguetis).

Aquí hay una lista incompleta de las cosas a tener en cuenta

Diseño de dominio deficiente (agregue la raíz ..., etc.)

Separación deficiente de las inquietudes (composición del servicio, comandos, consultas) Consulte CQRS y aprovisionamiento de eventos.

O Mappers (tenga cuidado, estas cosas pueden llevarlo a problemas)

Ver modelos y otros DTO (¡¡Nunca reutilices uno, y trata de mantenerlos al mínimo !!!!

Mario
fuente
2

Problema:

1) Constructor con una lista de parámetros cada vez mayor.

2) Si la clase se hereda (Ej .:), RepositoryBaseentonces cambiar la firma del constructor provoca un cambio en las clases derivadas.

Solución 1

Pase IoC Containeral constructor

Por qué

  • No más lista de parámetros cada vez mayor
  • La firma del constructor se vuelve simple

Por qué no

  • Hace que su clase esté estrechamente acoplada al contenedor IoC. (Eso causa problemas cuando 1. desea usar esa clase en otros proyectos en los que usa un contenedor IoC diferente. 2. decide cambiar el contenedor IoC)
  • Hace que tu clase sea menos descriptiva. (Realmente no se puede ver el constructor de clases y decir lo que necesita para funcionar).
  • La clase puede acceder potencialmente a todos los servicios.

Solución 2

Cree una clase que agrupe todos los servicios y páselo al constructor

 public abstract class EFRepositoryBase 
 {
    public class Dependency
    {
        public DbContext DbContext { get; }
        public IAuditFactory AuditFactory { get; }

         public Dependency(
            DbContext dbContext,
            IAuditFactory auditFactory)
        {
            DbContext = dbContext;
            AuditFactory = auditFactory;
        }
    }

    protected readonly DbContext DbContext;        
    protected readonly IJobariaAuditFactory auditFactory;

    protected EFRepositoryBase(Dependency dependency)
    {
        DbContext = dependency.DbContext;
        auditFactory= dependency.JobariaAuditFactory;
    }
  }

Clase derivada

  public class ApplicationEfRepository : EFRepositoryBase      
  {
     public new class Dependency : EFRepositoryBase.Dependency
     {
         public IConcreteDependency ConcreteDependency { get; }

         public Dependency(
            DbContext dbContext,
            IAuditFactory auditFactory,
            IConcreteDependency concreteDependency)
        {
            DbContext = dbContext;
            AuditFactory = auditFactory;
            ConcreteDependency = concreteDependency;
        }
     }

      IConcreteDependency _concreteDependency;

      public ApplicationEfRepository(
          Dependency dependency)
          : base(dependency)
      { 
        _concreteDependency = dependency.ConcreteDependency;
      }
   }

Por qué

  • Agregar una nueva dependencia a la clase no afecta las clases derivadas
  • La clase es independiente de IoC Container
  • La clase es descriptiva (en el aspecto de sus dependencias). Por convención, si desea saber de qué clase Adepende, esa información se acumula enA.Dependency
  • La firma del constructor se vuelve simple

Por qué no

  • necesita crear clase adicional
  • el registro del servicio se vuelve complejo (debe registrarse cada uno por X.Dependencyseparado)
  • Conceptualmente lo mismo que pasar IoC Container
  • ..

Sin embargo, la solución 2 es solo cruda, si hay un argumento sólido en su contra, se agradecería un comentario descriptivo

tchelidze
fuente
1

Este es el enfoque que uso

public class Hero
{

    [Inject]
    private IInventory Inventory { get; set; }

    [Inject]
    private IArmour Armour { get; set; }

    [Inject]
    protected IWeapon Weapon { get; set; }

    [Inject]
    private IAction Jump { get; set; }

    [Inject]
    private IInstanceProvider InstanceProvider { get; set; }


}

Aquí hay un enfoque burdo sobre cómo realizar las inyecciones y ejecutar el constructor después de inyectar valores. Este es un programa completamente funcional.

public class InjectAttribute : Attribute
{

}


public class TestClass
{
    [Inject]
    private SomeDependency sd { get; set; }

    public TestClass()
    {
        Console.WriteLine("ctor");
        Console.WriteLine(sd);
    }
}

public class SomeDependency
{

}


class Program
{
    static void Main(string[] args)
    {
        object tc = FormatterServices.GetUninitializedObject(typeof(TestClass));

        // Get all properties with inject tag
        List<PropertyInfo> pi = typeof(TestClass)
            .GetProperties(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public)
            .Where(info => info.GetCustomAttributes(typeof(InjectAttribute), false).Length > 0).ToList();

        // We now happen to know there's only one dependency so we take a shortcut just for the sake of this example and just set value to it without inspecting it
        pi[0].SetValue(tc, new SomeDependency(), null);


        // Find the right constructor and Invoke it. 
        ConstructorInfo ci = typeof(TestClass).GetConstructors()[0];
        ci.Invoke(tc, null);

    }
}

Actualmente estoy trabajando en un proyecto de hobby que funciona así https://github.com/Jokine/ToolProject/tree/Core

Ronijo
fuente
-8

¿Qué marco de inyección de dependencia está utilizando? ¿Has intentado usar una inyección basada en setter?

El beneficio para la inyección basada en constructor es que parece natural para los programadores de Java que no usan marcos DI. Necesita 5 cosas para inicializar una clase, luego tiene 5 argumentos para su constructor. La desventaja es lo que has notado, se vuelve difícil de manejar cuando tienes muchas dependencias.

Con Spring, podría pasar los valores requeridos con setters en su lugar y podría usar las anotaciones @required para exigir que se inyecten. La desventaja es que debe mover el código de inicialización del constructor a otro método y hacer que Spring lo llame después de que se inyecten todas las dependencias marcándolo con @PostConstruct. No estoy seguro acerca de otros marcos, pero supongo que hacen algo similar.

Ambas formas funcionan, es una cuestión de preferencia.

David W Crook
fuente
21
La razón para la inyección del constructor es hacer obvias las dependencias, no porque parezca más natural para los desarrolladores de Java.
L-Four
8
Comentario tardío, pero esta respuesta me hizo reír :)
Frederik Prijck
1
+1 para inyecciones basadas en setter. Si tengo servicios y repositorios definidos en mi clase, es bastante obvio que son dependencias. No necesito escribir constructores de aspecto VB6 masivos, y hacer un código de asignación estúpido en el constructor. Es bastante obvio cuáles son las dependencias en los campos requeridos.
Piotr Kula
Según 2018, Spring recomienda oficialmente no usar la inyección de setter excepto para las dependencias que tienen un valor predeterminado razonable. Como en, si la dependencia es obligatoria para la clase, se recomienda la inyección del constructor. Ver discusión sobre setter vs ctor DI
John Doe