Iniezione delle dipendenze; buone pratiche per ridurre il codice boilerplate

25

Ho una domanda semplice, e non sono nemmeno sicuro che abbia una risposta, ma proviamo. Sto codificando in C ++ e uso l'iniezione di dipendenza per evitare lo stato globale. Funziona abbastanza bene, e non corro spesso in comportamenti inaspettati / indefiniti.

Tuttavia, mi rendo conto che, mentre il mio progetto cresce, sto scrivendo un sacco di codice che considero boilerplate. Peggio ancora: il fatto che ci sia più codice standard, il codice reale lo rende a volte difficile da capire.

Niente batte un buon esempio, quindi andiamo:

Ho una classe chiamata TimeFactory che crea oggetti Time.

Per ulteriori dettagli (non sono sicuro che sia rilevante): gli oggetti temporali sono piuttosto complessi perché il Tempo può avere diversi formati e la conversione tra loro non è né lineare né semplice. Ogni "Ora" contiene un sincronizzatore per gestire le conversioni e per assicurarsi che abbiano lo stesso sincronizzatore, correttamente inizializzato, uso un TimeFactory. TimeFactory ha solo un'istanza ed è esteso a tutte le applicazioni, quindi potrebbe qualificarsi per Singleton ma, poiché è mutabile, non voglio renderlo un singleton

Nella mia app, molte classi hanno bisogno di creare oggetti Time. A volte queste classi sono profondamente annidate.

Diciamo che ho una classe A che contiene istanze di classe B, e così via fino alla classe D. La classe D deve creare oggetti Time.

Nella mia ingenua implementazione, passo TimeFactory al costruttore della classe A, che lo passa al costruttore della classe B e così via fino alla classe D.

Ora, immagino di avere un paio di classi come TimeFactory e un paio di gerarchie di classi come quella di cui sopra: perdo tutta la flessibilità e la leggibilità che suppongo utilizzi l'iniezione di dipendenza.

Sto iniziando a chiedermi se non ci sia un difetto di progettazione nella mia app ... O è questo il male necessario per usare l'iniezione di dipendenza?

Che ne pensi?

    
posta Dinaiz 05.08.2012 - 05:46
fonte

6 risposte

22

In my app, a lot of classes need to create Time objects

Sembra che la tua classe Time sia un tipo di dati di base che dovrebbe appartenere all '"infrastruttura generale" della tua applicazione. DI non funziona bene per tali classi. Pensa a cosa significa se una classe come string deve essere iniettata in ogni parte del codice che utilizza le stringhe e dovrai utilizzare un stringFactory come unica possibilità di creare nuove stringhe: la leggibilità del tuo programma diminuirebbe di un ordine di grandezza.

Quindi il mio suggerimento: non usare DI per tipi di dati generali come Time . Scrivi test unitari per la classe Time stessa, e una volta terminato, usalo ovunque nel tuo programma, proprio come la classe string , o una classe vector o qualsiasi altra classe della lib standard. Usa DI per componenti che dovrebbero essere realmente disaccoppiati l'uno dall'altro.

    
risposta data 05.08.2012 - 13:15
fonte
4

Che cosa intendi con "Perdo tutta la flessibilità e la leggibilità che suppongo utilizzi l'iniezione di dipendenza" - DI non si tratta di leggibilità. Si tratta di disaccoppiare la dipendenza tra gli oggetti.

Sembra che tu abbia la Classe A che crea la Classe B, la Classe B che crea la Classe C e la Classe C che crea la Classe D.

Ciò che dovresti avere è la Classe B iniettata nella Classe A. Classe C iniettata nella Classe B. Classe D iniettata nella Classe C.

    
risposta data 05.08.2012 - 12:15
fonte
3

Non sono sicuro del motivo per cui non vuoi rendere il tuo time factory un singleton. Se c'è una sola istanza in tutta la tua app, è di fatto un singleton.

Detto questo, è molto pericoloso condividere un oggetto mutabile, tranne se è adeguatamente sorvegliato sincronizzando i blocchi, nel qual caso, non c'è motivo per cui non sia un singleton.

Se vuoi fare un'iniezione di dipendenze, potresti voler esaminare la primavera o altri framework di iniezione delle dipendenze, che ti consentirebbero di assegnare automaticamente i parametri usando un'annotazione

    
risposta data 05.08.2012 - 10:31
fonte
1

Questo vuole essere una risposta complementare a Doc Brown, e anche rispondere ai commenti senza risposta di Dinaiz che sono ancora legati alla Domanda.

Ciò di cui hai probabilmente bisogno è un framework per fare DI. Avere gerarchie complesse non significa necessariamente una progettazione errata, ma se si deve iniettare un bottom-up TimeFactory (da A a D) invece di iniettare direttamente in D allora probabilmente c'è qualcosa di sbagliato nel modo in cui si sta facendo Dependency Injection.

Un singleton? No grazie. Se hai bisogno di una sola istanza, rendila condivisa nel tuo contesto applicativo (utilizzando un contenitore IoC per DI come Infector ++ richiede solo il binding di TimeFactory come istanza singola), ecco l'esempio (C ++ 11 a proposito, ma così C ++. a C ++ 11 già? Si ottiene l'applicazione Leak-Free gratuitamente):

Infector::Container ioc; //your app's context

ioc.bindSingleAsNothing<TimeFactory>(); //declare TimeFactory to be shared
ioc.wire<TimeFactory>(); //wire its constructor 

// if you want to be sure TimeFactory is created at startup just request it
// (else it will be created lazily only when needed)
auto myTimeFactory = ioc.buildSingle<TimeFactory>();

Ora il punto buono di un contenitore IoC è che non è necessario passare il tempo in fabbrica a D. se la classe "D" ha bisogno di una produzione a tempo, inserire semplicemente la produzione di fabbrica come parametro costruttore per la classe D.

ioc.bindAsNothing<A>(); //declare class A
ioc.bindAsNothing<B>(); //declare class B
ioc.bindAsNothing<D>(); //declare class D

//constructors setup
ioc.wire<D, TimeFactory>(); //time factory injected to class D
ioc.wire<B, D>(); //class D injected to class B
ioc.wire<A, B>(); //class B injected to class A

come ti vedi iniettare TimeFactory solo una volta. Come usare "A"? Molto semplice, ogni classe viene iniettata, costruita nel principale o istanziata con una fabbrica.

auto myA1 = ioc.build<A>(); //A is not "single" so many different istances
auto myA2 = ioc.build<A>(); //can live at same time

ogni volta che crei la classe A sarà automaticamente (lazy instantiation) iniettata con tutte le dipendenze fino a D e D verrà iniettata con TimeFactory, quindi chiamando solo 1 metodo avrai la tua gerarchia completa pronta (e anche le gerarchie complesse sono risolto in questo modo rimuovendo un sacco di codice piastra della caldaia): non devi chiamare "nuovo / cancella" e questo è molto importante perché puoi separare la logica dell'applicazione dal codice della colla.

D can create Time objects with informations that only D can have

È facile, il tuo TimeFactory ha un metodo "crea", quindi usa solo una firma diversa "create (params)" e hai finito. I parametri che non dipendono sono spesso risolti in questo modo. Questo elimina anche il dovere di iniettare cose come "stringhe" o "interi" perché questo aggiunge solo un piatto di caldaia aggiuntivo.

Chi crea chi? Il contenitore IoC crea istanze e fabbriche, le fabbriche creano il resto (le fabbriche possono creare oggetti diversi con parametri arbitrari, quindi non hai realmente bisogno di uno stato per le fabbriche). È ancora possibile utilizzare le fabbriche come wrapper per il contenitore IoC: in generale, injectin il contenitore IoC è molto cattivo ed è lo stesso dell'utilizzo di un localizzatore di servizi. Alcune persone hanno risolto il problema avvolgendo il Contenitore IoC con una fabbrica (questo non è strettamente necessario, ma ha il vantaggio che la gerarchia è risolta dal Container e tutte le tue fabbriche diventano ancora più facili da mantenere).

//factory method
std::unique_ptr<myType> create(params){
    auto istance = ioc->build<myType>(); //this code's agnostic to "myType" hierarchy
    istance->setParams(params); //the customization you needed
    return std::move(istance); 
}

Inoltre non abusare delle dipendenze, i tipi semplici possono essere solo membri di classe o variabili locali. Questo sembra ovvio, ma ho visto persone iniettare "std :: vector" solo perché c'era un framework DI che lo permetteva. Ricorda sempre la legge di Demeter: "Inietta solo ciò che hai veramente bisogno di iniettare"

    
risposta data 21.07.2013 - 13:39
fonte
0

Le tue classi A, B e C devono anche creare istanze temporali o solo la classe D? Se è solo la classe D, allora A e B non dovrebbero sapere nulla di TimeFactory. Creare un'istanza di TimeFactory all'interno della classe C e passarla alla classe D. Si noti che con "creare un'istanza" non necessariamente si intende che la classe C debba essere responsabile dell'istanziazione di TimeFactory. Può ricevere DClassFactory dalla classe B e DClassFactory sa come creare l'istanza Time.

Una tecnica che uso spesso anche quando non ho un framework DI fornisce due costruttori, uno che accetta una fabbrica e un altro che crea una fabbrica predefinita. Il secondo ha di solito un accesso protetto / pacchetto, ed è usato principalmente per i test unitari.

    
risposta data 05.08.2012 - 13:32
fonte
-1

Ho implementato un'altra dipendenza C ++ quadro dell'iniezione, che recentemente è stato proposto per il potenziamento - link - libreria è macro less (gratuito), solo header, libreria C ++ 03 / C ++ 11 / C ++ 14 che fornisce un tipo sicuro, tempo di compilazione, iniezione dipendenza dipendenza costruttore macro.

    
risposta data 25.07.2014 - 13:59
fonte

Leggi altre domande sui tag