2012-02-06 7 views
30

Non mi piace l'iniezione delle dipendenze basata sul costruttore.Un'alternativa valida all'iniezione di dipendenza?

Credo che aumenti la complessità del codice e diminuisca la manutenibilità e mi piacerebbe sapere se esistono alternative valide.

Non sto parlando del concetto di separazione dell'implementazione dall'interfaccia e di un modo per risolvere dinamicamente (in modo ricorsivo) un insieme di oggetti da un'interfaccia. Lo sostengo completamente. Tuttavia, il modo tradizionale di costruire questo metodo sembra avere alcuni problemi.

1) Tutti i test dipendono dai costruttori.

Dopo aver utilizzato DI ampiamente in un 3 C# progetto MVC nel corso dell'ultimo anno, trovo il nostro codice pieno di cose come questa:

public interface IMyClass { 
    ... 
} 

public class MyClass : IMyClass { 

    public MyClass(ILogService log, IDataService data, IBlahService blah) { 
    _log = log; 
    _blah = blah; 
    _data = data; 
    } 

    ... 
} 

Problema: Se ho bisogno di un altro servizio nella mia implementazione devo modificare il costruttore; e ciò significa che tutti i test unitari per questa interruzione di classe.

Anche i test che non hanno nulla a che fare con la nuova funzionalità richiedono almeno un refactoring per aggiungere parametri aggiuntivi e iniettare una simulazione per tale argomento.

Questo sembra un piccolo problema e strumenti automatici come l'aiuto del programma di ricerca, ma è certamente fastidioso quando un semplice cambiamento come questo causa la rottura di più di 100 test. In pratica ho visto persone fare cose stupide per evitare di cambiare il costruttore piuttosto che mordere il proiettile e correggere tutti i test quando questo accade.

2) Le istanze del servizio vengono passate inutilmente, aumentando la complessità del codice.

public class MyWorker { 
    public MyWorker(int x, IMyClass service, ILogService logs) { 
    ...  
    } 
} 

Creazione di un'istanza di questa classe, se possibile, solo se io sono all'interno di un contesto in cui i servizi forniti sono disponibili e sono stati automaticamente risolti (ad es. Controllore) o, purtroppo, facendo passare l'istanza del servizio verso il basso più catene di classi di aiuto.

vedo codice come questo tutto il tempo:

public class BlahHelper { 

    // Keep so we can create objects later 
    var _service = null; 

    public BlahHelper(IMyClass service) { 
    _service = service; 
    } 

    public void DoSomething() { 
    var worker = new SpecialPurposeWorker("flag", 100, service); 
    var status = worker.DoSomethingElse(); 
    ... 

    } 
} 

Nel caso in cui l'esempio non è chiaro, quello di cui sto parlando è passando casi di interfaccia DI risolti verso il basso attraverso strati multipli da nessun motivo diversi dal livello inferiore sono necessari per iniettare qualcosa.

Se una classe non dipende da un servizio, dovrebbe avere una dipendenza da tale servizio. Questa idea che esiste una dipendenza 'transitoria' in cui una classe non utilizza un servizio ma semplicemente la trasmette è, a mio parere, un'assurdità.

Tuttavia, non conosco una soluzione migliore.

C'è qualcosa che fornisce i vantaggi di DI senza questi problemi?

ho contemplato utilizzando il framework DI all'interno dei costruttori invece come questo consente di risolvere un paio di problemi:

public MyClass() { 
    _log = Register.Get().Resolve<ILogService>(); 
    _blah = Register.Get().Resolve<IBlahService>(); 
    _data = Register.Get().Resolve<IDataService>(); 
} 

C'è qualche aspetto negativo di fare questo?

Significa che i test di unità devono avere "conoscenza preliminare" della classe per associare i mock ai tipi corretti durante il test init, ma non vedo altri aspetti negativi.

NB. I miei esempi sono in C#, ma sono incappato negli stessi problemi anche in altri linguaggi, e in particolare le lingue con supporto di strumenti meno maturi sono i principali grattacapi.

+0

se lo si inserisce nel costruttore si crea una dipendenza dal componente di usato. se passi le interfacce al costruttore puoi cambiare il di componente diciamo Autofac con NInject o Castle ecc. Quindi se sei veramente sicuro che non cambierai mai il componente di usato e rimani su, diciamo che Autofac è una soluzione molto valida. –

risposta

17

A mio parere, la causa principale di tutti i problemi non sta funzionando correttamente. Lo scopo principale dell'utilizzo del DI di costruzione è quello di indicare chiaramente tutte le dipendenze di alcune classi. Se qualcosa dipende da qualcosa, hai sempre due scelte: rendere esplicita questa dipendenza o nasconderla in qualche meccanismo (in questo modo tende a portare più grattacapi che profitti).

Vediamo passare attraverso le sue dichiarazioni:

Tutti i test dipendono dai costruttori.

[snip]

Problema: se ho bisogno di un altro servizio nella mia implementazione devo modificare il costruttore; e ciò significa che tutti i test unitari per questa interruzione di classe.

Fare in modo che una classe dipenda da qualche altro servizio è un cambiamento piuttosto importante. Se si dispone di diversi servizi che implementano la stessa funzionalità, direi che esiste un problema di progettazione. Correggere il mocking e fare test soddisfano SRP (che in termini di test unitari si riduce a "Scrivi un test separato per ogni caso di test") e indipendente dovrebbe risolvere questo problema.

2) Le istanze di servizio vengono passate inutilmente, aumentando la complessità del codice.

Uno degli usi più generali del DI consiste nel separare la creazione dell'oggetto dalla logica aziendale. Nel tuo caso vediamo che ciò di cui hai veramente bisogno è creare un lavoratore che a sua volta ha bisogno di diverse dipendenze che vengono iniettate attraverso l'intero grafico dell'oggetto. L'approccio migliore per risolvere questo problema è di non eseguire mai alcun new nella logica aziendale. Per tali casi preferirei iniettare una fabbrica di lavoratori che estrae il codice commerciale dalla creazione effettiva del lavoratore.

ho contemplato utilizzando il framework DI all'interno dei costruttori invece come questo consente di risolvere un paio di problemi:

public MyClass() { 
    _log = Register.Get().Resolve<ILogService>(); 
    _blah = Register.Get().Resolve<IBlahService>(); 
    _data = Register.Get().Resolve<IDataService>(); 
} 

C'è qualche aspetto negativo di fare questo?

Come beneficio si otterrà tutti gli aspetti negativi di utilizzare il modello Singleton (codice non verificabile e un enorme spazio di stato della vostra applicazione).

Quindi direi che DI dovrebbe essere fatto bene (come qualsiasi altro strumento). La soluzione ai tuoi problemi (IMO) sta nella comprensione di DI e della formazione dei membri del tuo team.

+4

'Register.Get(). Resolve <>()' è un perfetto esempio di [ServiceLocator anti-pattern] (http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx). Se aggiungere un'ulteriore dipendenza a un servizio comporta la violazione di molti casi di test, è necessario refactoring dei test. Come suggerito da @Alex, farle seguire SRP è una cosa. Un altro sarebbe utilizzare qualcosa come [TestInitializeAttribute] (http://msdn.microsoft.com/en-us/library/microsoft.visualstudio.testtools.unittesting.testinitializeattribute.aspx) (se si sta utilizzando MsTest) per inserire configurazione principale dei tuoi test in un unico posto. –

+0

Utile. "Sii più educato". Come è questa la risposta accettata? –

13

Si è tentati di commettere Iniezione del Costruttore per questi problemi, ma in realtà sono sintomi di un'implementazione impropria più degli svantaggi di Iniezione del Costruttore.

Diamo un'occhiata a ciascun problema apparente individualmente.

Tutti i test dipendono dai costruttori.

Il problema qui è che i test di unità sono strettamente accoppiati ai costruttori. Questo può spesso essere risolto con un semplice SUT Factory - un concetto che può essere esteso in un Auto-mocking Container.

In ogni caso quando si utilizza Iniezione costruttore, constructors should be simple, quindi non c'è motivo di testarli direttamente. Sono dettagli di implementazione che si presentano come effetti collaterali dei test comportamentali che scrivi.

Le istanze di servizio vengono passate inutilmente, aumentando la complessità del codice.

D'accordo, questo è certamente un odore di codice, ma ancora una volta, l'odore è nella realizzazione. Costruttore Iniezione non può essere incolpato per questo.

In questo caso è un sintomo che manca uno Facade Service. Invece di fare questo:

public class BlahHelper { 

    // Keep so we can create objects later 
    var _service = null; 

    public BlahHelper(IMyClass service) { 
     _service = service; 
    } 

    public void DoSomething() { 
     var worker = new SpecialPurposeWorker("flag", 100, service); 
     var status = worker.DoSomethingElse(); 
     // ... 

    } 
} 

fare questo:

public class BlahHelper { 
    private readonly ISpecialPurposeWorkerFactory factory; 

    public BlahHelper(ISpecialPurposeWorkerFactory factory) { 
     this.factory = factory; 
    } 

    public void DoSomething() { 
     var worker = this.factory.Create("flag", 100); 
     var status = worker.DoSomethingElse(); 
     // ... 

    } 
} 

Per quanto riguarda la soluzione proposta

La soluzione proposta è un Locator Service e it has only disadvantages and no benefits.

+0

e il servizio di registrazione? è un problema che viene iniettato in ogni classe ... – danfromisrael

+2

La registrazione non è un servizio: è una preoccupazione trasversale: http://stackoverflow.com/questions/7905110/logging-aspect-orientated-programming-and-dependency -injection-trying-to-make/7906547 # 7906547 –

+0

Mi piacciono le tue soluzioni, ma sembra sostenere l'uso del modello factory anziché DI; Sto bene, ma potresti approfondire perché è meglio? Ci sono aspetti negativi (ad esempio coperto qui: http://code.google.com/p/google-guice/wiki/Motivation) con pattern factory che le persone hanno già trattato in precedenza. – Doug

Problemi correlati