Come evitare la follia del costruttore dell'iniezione di dipendenza?


300

Trovo che i miei costruttori stiano iniziando ad apparire così:

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

con un elenco di parametri sempre crescente. Poiché "Container" è il mio contenitore di iniezione di dipendenza, perché non posso semplicemente fare questo:

public MyClass(Container con)

per ogni classe? Quali sono gli svantaggi? Se lo faccio, mi sembra di usare una statica glorificata. Per favore, condividi le tue opinioni sulla follia dell'IoC e dell'iniezione di dipendenza.


64
perché stai passando il container? Penso che potresti fraintendere il CIO
Paul Creasey il

33
Se i tuoi costruttori richiedono più o più parametri, potresti fare troppo in quelle classi.
Austin Salonen,

38
Non è così che fai l'iniezione del costruttore. Gli oggetti non conoscono affatto il contenitore IoC, né dovrebbero.
duffymo,

Puoi semplicemente creare un costruttore vuoto, in cui chiami il DI chiedendo direttamente le cose di cui hai bisogno. Ciò rimuoverà il costruttore Madnes, ma è necessario assicurarsi di utilizzare un'interfaccia DI .. nel caso in cui si modifichi il sistema DI a metà sviluppo. Onestamente ... nessuno tornerà a farlo in questo modo, anche se questo è ciò che DI fa per iniettare nel tuo costruttore. doh
Piotr Kula,

Risposte:


409

Hai ragione se usi il container come Service Locator, è più o meno una fabbrica statica glorificata. Per molte ragioni lo considero un anti-pattern .

Uno dei meravigliosi benefici dell'iniezione del costruttore è che rende palesemente evidenti le violazioni del principio della responsabilità singola .

Quando ciò accade, è il momento di fare il refactoring ai servizi di facciata . In breve, creare una nuova interfaccia più a grana grossa che nasconda l'interazione tra alcune o tutte le dipendenze a grana fine attualmente richieste.


8
+1 per quantificare lo sforzo di refactoring in un unico concetto; fantastico :)
Ryan Emerle

46
davvero? hai appena creato una indiretta per spostare quei parametri in un'altra classe, ma sono ancora lì! solo più complicato da gestire.
indiscutibile l'

23
@irreputable: Nel caso degenerato in cui trasferiamo tutte le dipendenze in un servizio aggregato, sono d'accordo che è solo un altro livello di riferimento indiretto che non porta alcun beneficio, quindi la mia scelta delle parole era leggermente fuori. Tuttavia, il punto è che trasferiamo solo alcune delle dipendenze a grana fine in un servizio aggregato. Ciò limita il numero di permutazioni di dipendenza sia nel nuovo servizio aggregato sia per le dipendenze lasciate indietro. Questo rende entrambi più semplici da gestire.
Mark Seemann l'

92
La migliore osservazione: "Uno dei meravigliosi benefici dell'iniezione del costruttore è che rende palesemente evidenti le violazioni del principio della responsabilità singola".
Igor Popov,

2
@DonBox In tal caso è possibile scrivere implementazioni di oggetti null per interrompere la ricorsione. Non quello di cui hai bisogno, ma il punto è che l'iniezione del costruttore non impedisce i cicli, ma chiarisce solo che sono lì.
Mark Seemann,

66

Non credo che i costruttori della tua classe dovrebbero avere un riferimento al periodo del tuo contenitore IOC. Ciò rappresenta una dipendenza non necessaria tra la classe e il contenitore (il tipo di IOC delle dipendenze sta cercando di evitare!).


+1 A seconda di un contenitore IoC, diventa difficile cambiare quel contenitore in un secondo momento senza cambiare gruppo di codice in tutte le altre classi
Tseng

1
Come implementeresti IOC senza avere parametri di interfaccia sui costruttori? Sto leggendo il tuo post sbagliato?
J Hunt,

@J Hunt Non capisco il tuo commento. Per me, i parametri di interfaccia significano parametri che sono interfacce per le dipendenze, cioè se il contenitore di iniezione delle dipendenze si inizializza MyClass myClass = new MyClass(IDependency1 interface1, IDependency2 interface2)(parametri di interfaccia). Questo non è correlato al post di @ derivation, che interpreto dicendo che un contenitore di iniezione di dipendenza non dovrebbe iniettarsi nei suoi oggetti, vale a direMyClass myClass = new MyClass(this)
John Doe,

25

La difficoltà di passare i parametri non è il problema. Il problema è che la tua classe sta facendo troppo e che dovrebbe essere suddivisa di più.

L'iniezione di dipendenza può agire come un avvertimento precoce per le classi che diventano troppo grandi, in particolare a causa del dolore crescente di passare in tutte le dipendenze.


44
Correggimi se sbaglio, ma a un certo punto devi 'incollare tutto insieme', e quindi devi ottenere più di alcune dipendenze per quello. Ad esempio nel livello Visualizza, quando si creano modelli e dati per loro, è necessario acquisire tutti i dati da varie dipendenze (ad esempio "servizi") e quindi inserire tutti questi dati nel modello e sullo schermo. Se la mia pagina web ha 10 diversi "blocchi" di informazioni, quindi ho bisogno di 10 classi diverse per fornirmi quei dati. Quindi ho bisogno di 10 dipendenze nella mia classe View / Template?
Andrew,

4

Mi sono imbattuto in una domanda simile sull'iniezione di dipendenza basata sul costruttore e su quanto fosse complessa passare in tutte le dipendenze.

Uno degli approcci che ho usato in passato è usare il modello di facciata dell'applicazione usando un livello di servizio. Ciò avrebbe un'API grossolana. Se questo servizio dipende dai repository, utilizzerebbe un'iniezione setter delle proprietà private. Ciò richiede la creazione di una fabbrica astratta e lo spostamento della logica di creazione dei repository in una fabbrica.

Codice dettagliato con spiegazione può essere trovato qui

Best practice per l'IoC in un livello di servizio complesso


3

Ho letto tutto questo thread, due volte, e penso che le persone rispondano a ciò che sanno, non a ciò che viene chiesto.

La domanda originale di JP sembra che stia costruendo oggetti inviando un resolver, e poi un gruppo di classi, ma stiamo assumendo che quelle classi / oggetti siano essi stessi servizi, pronti per l'iniezione. E se non lo fossero?

JP, se stai cercando di sfruttare DI e desideri la gloria di mescolare l'iniezione con dati contestuali, nessuno di questi schemi (o presunti "anti-schemi") si rivolge specificamente a quello. In realtà si riduce all'utilizzo di un pacchetto che ti supporterà in tale sforzo.

Container.GetSevice<MyClass>(someObject1, someObject2)

... questo formato è raramente supportato. Credo che la difficoltà di programmare tale supporto, aggiunta alla miserabile performance che sarebbe associata all'implementazione, lo rende poco attraente per gli sviluppatori open source.

Ma dovrebbe essere fatto, perché dovrei essere in grado di creare e registrare una factory per MyClass'es, e quella factory dovrebbe essere in grado di ricevere dati / input che non sono spinti ad essere un "servizio" solo per il gusto di passare dati. Se "l'anti-pattern" riguarda conseguenze negative, forzare l'esistenza di tipi di servizi artificiali per il passaggio di dati / modelli è certamente negativo (alla pari con la sensazione di avvolgere le classi in un contenitore. Lo stesso istinto si applica).

Ci sono framework che possono aiutare, anche se sembrano un po 'brutti. Ad esempio, Ninject:

Creazione di un'istanza utilizzando Ninject con parametri aggiuntivi nel costruttore

Questo è per .NET, è popolare e non è ancora così pulito come dovrebbe essere, ma sono sicuro che ci sia qualcosa in qualunque lingua tu scelga di utilizzare.


3

L'iniezione del contenitore è una scorciatoia di cui ti pentirai.

L'eccessiva iniezione non è il problema, di solito è un sintomo di altri difetti strutturali, in particolare la separazione delle preoccupazioni. Questo non è un problema, ma può avere molte fonti e ciò che rende così difficile da risolvere è che dovrai affrontarli tutti, a volte allo stesso tempo (pensa a districare gli spaghetti).

Ecco un elenco incompleto delle cose da cercare

Progettazione di domini scadenti (radice aggregata ... ecc.)

Scarsa separazione delle preoccupazioni (composizione del servizio, comandi, query) Vedi CQRS ed Event Sourcing.

OR Mapper (fai attenzione, queste cose possono metterti nei guai)

Guarda i modelli e gli altri DTO (non riutilizzarli mai e cerca di ridurli al minimo !!!!)


2

Problema:

1) Costruttore con un elenco di parametri sempre crescente.

2) Se la classe viene ereditata (Es . RepositoryBase:), la modifica della firma del costruttore provoca il cambiamento nelle classi derivate.

Soluzione 1

Passa IoC Containeral costruttore

Perché

  • Non è più necessario aumentare l'elenco dei parametri
  • La firma del costruttore diventa semplice

Perchè no

  • Ti rende la classe strettamente accoppiata al contenitore IoC. (Ciò causa problemi quando 1. si desidera utilizzare quella classe in altri progetti in cui si utilizza un contenitore IoC diverso. 2. si decide di modificare il contenitore IoC)
  • Ti rende la classe meno descrittiva. (Non puoi davvero guardare il costruttore di classi e dire di cosa ha bisogno per funzionare.)
  • La classe può accedere potenzialmente a tutti i servizi.

Soluzione 2

Crea una classe che raggruppa tutti i servizi e passali al costruttore

 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;
    }
  }

Classe derivata

  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;
      }
   }

Perché

  • L'aggiunta di nuova dipendenza alla classe non influisce sulle classi derivate
  • La classe è indipendente da IoC Container
  • La classe è descrittiva (in aspetto delle sue dipendenze). Per convenzione, se vuoi sapere da quale classe Adipende, le informazioni vengono accumulateA.Dependency
  • La firma del costruttore diventa semplice

Perchè no

  • è necessario creare classe aggiuntiva
  • la registrazione del servizio diventa complessa (è necessario registrarsi X.Dependencyseparatamente)
  • Concettualmente uguale al passaggio IoC Container
  • ..

La soluzione 2 è solo una questione grezza, se ci fossero solidi argomenti contro di essa, il commento descrittivo sarebbe apprezzato


1

Questo è l'approccio che 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; }


}

Ecco un approccio approssimativo su come eseguire le iniezioni ed eseguire il costruttore dopo l'iniezione di valori. Questo è un programma completamente funzionale.

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);

    }
}

Attualmente sto lavorando a un progetto di hobby che funziona in questo modo https://github.com/Jokine/ToolProject/tree/Core


-8

Quale framework di iniezione di dipendenza stai usando? Hai provato a usare l'iniezione basata sul setter?

Il vantaggio dell'iniezione basata sul costruttore è che sembra naturale per i programmatori Java che non usano framework DI. Hai bisogno di 5 cose per inizializzare una classe, quindi hai 5 argomenti per il tuo costruttore. L'aspetto negativo è quello che hai notato, diventa ingombrante quando hai molte dipendenze.

Con Spring puoi invece passare i valori richiesti con i setter e puoi usare le annotazioni @required per imporre che vengano iniettate. Il rovescio della medaglia è che è necessario spostare il codice di inizializzazione dal costruttore in un altro metodo e fare in modo che Spring chiami che dopo che tutte le dipendenze sono state iniettate contrassegnandolo con @PostConstruct. Non sono sicuro di altri framework ma presumo che facciano qualcosa di simile.

Entrambe le modalità di lavoro, è una questione di preferenza.


21
Il motivo dell'iniezione del costruttore è di rendere ovvie le dipendenze, non perché sembra più naturale per gli sviluppatori Java.
L-Four

8
Commento in ritardo, ma questa risposta mi ha fatto ridere :)
Frederik Prijck

1
+1 per iniezioni basate sul setter. Se ho servizi e repository definiti nella mia classe, è abbastanza ovvio che sono dipendenze .. Non ho bisogno di scrivere enormi costruttori dall'aspetto VB6 e fare stupido codice di assegnazione nel costruttore. È abbastanza ovvio quali siano le dipendenze dai campi richiesti.
Piotr Kula,

A partire dal 2018, Spring raccomanda ufficialmente di non utilizzare l'iniezione setter, tranne per le dipendenze che hanno un valore predefinito ragionevole. Come in, se la dipendenza è obbligatoria per la classe, si consiglia l'iniezione del costruttore. Vedi discussione su setter vs ctor DI
John Doe il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.