Iniezione di dipendenza ; buone pratiche per ridurre il codice del boilerplate


25

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

Tuttavia mi rendo conto che, man mano che il mio progetto cresce, sto scrivendo un sacco di codice che considero piatto di caldaia. Peggio ancora: il fatto che ci sia più codice boilerplate rispetto al codice effettivo rende talvolta difficile la comprensione.

Niente batte un buon esempio quindi andiamo:

Ho una classe chiamata TimeFactory che crea oggetti Time.

Per maggiori dettagli (non sono sicuro che sia rilevante): gli oggetti Time sono piuttosto complessi perché il Tempo può avere formati diversi e la conversione tra di essi non è né lineare, né diretta. Ogni "Time" contiene un Synchronizer per gestire le conversioni e per assicurarsi che abbiano lo stesso sincronizzatore correttamente inizializzato, utilizzo TimeFactory. TimeFactory ha solo un'istanza ed è ampia a livello di applicazione, quindi si qualificherebbe per singleton ma, poiché è mutabile, non voglio renderlo un singleton

Nella mia app, molte classi devono creare oggetti Time. A volte quelle classi sono profondamente nidificate.

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 il TimeFactory al costruttore della classe A, che lo passa al costruttore della classe B e così via fino alla classe D.

Ora, immagina di avere un paio di classi come TimeFactory e un paio di gerarchie di classi come quella sopra: perdo tutta la flessibilità e la leggibilità che suppongo di ottenere usando la dipendenza.

Sto iniziando a chiedermi se nella mia app non ci sono grossi difetti di progettazione ... O è forse un male necessario usare l'iniezione di dipendenza?

Cosa pensi ?


5
Ho inviato questa domanda ai programmatori invece che allo stackoverflow perché, come ho capito, i programmatori sono più interessati alle queston legate al design e lo stackoverflow è per le domande "quando sei di fronte al compilatore". Scusa in anticipo se sbaglio.
Dinaiz,

2
programmazione orientata all'aspetto, è anche un bene per questo compito - en.wikipedia.org/wiki/Aspect-oriented_programming
EL Yusubov

La fabbrica di classe A è per le classi B, C e D? In caso contrario, perché ne crea istanze? Se solo la classe D necessita di oggetti Time, perché iniettare qualsiasi altra classe con TimeFactory? La classe D ha davvero bisogno di TimeFactory o potrebbe essere iniettata solo un'istanza di Time?
simoraman

D deve creare oggetti Time, con solo l'informazione che D dovrebbe avere. Devo pensare al resto del tuo commento. Potrebbe esserci un problema nel "chi sta creando chi" nel mio codice
Dinaiz,

"chi sta creando chi" è risolto dai contenitori IoC, vedere la mia risposta per i dettagli. "Chi sta creando chi" è una delle migliori domande che puoi porre quando usi l'iniezione di dipendenza.
GameDeveloper

Risposte:


23

Nella mia app, molte classi devono creare oggetti Time

Sembra che la tua Timeclasse sia un tipo di dati molto semplice che dovrebbe appartenere alla "infrastruttura generale" della tua applicazione. DI non funziona bene per tali classi. Pensa a cosa significherebbe se una classe simile stringdovesse essere iniettata in ogni parte del codice che utilizza le stringhe e avresti bisogno di usare a stringFactorycome unica possibilità di creare nuove stringhe: la leggibilità del tuo programma diminuirebbe di un ordine di grandezza.

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


Hai capito perfettamente. "la leggibilità del tuo programma diminuirebbe di un ordine di grandezza." : è esattamente quello che è successo sì. A condizione che io voglia ancora usare una fabbrica, non è poi così male renderla singleton?
Dinaiz,

3
@Dinaiz: l'idea è quella di accettare uno stretto accoppiamento tra Timee le altre parti del programma. Quindi puoi accettare anche accettare l'accoppiamento stretto a TimeFactory. Ciò che eviterei, tuttavia, è avere un singolo TimeFactoryoggetto globale con uno stato (ad esempio, informazioni sulla localizzazione o qualcosa del genere) - che potrebbe causare cattivi effetti collaterali al programma e rendere molto difficili i test generali e il riutilizzo. O renderlo apolide o non usarlo come singleton.
Doc Brown,

In realtà deve avere uno stato, quindi quando dici "Non usarlo come singleton", intendi "continuare a usare l'iniezione di dipendenza, cioè passare l'istanza a ogni oggetto che ne ha bisogno", giusto?
Dinaiz,

1
@Dinaiz: onestamente, dipende dal tipo di stato, dal tipo e dal numero di parti del programma che utilizzano Timee TimeFactory, dal grado di "evolvibilità" necessario per future estensioni correlate TimeFactorye così via.
Doc Brown,

OK proverò a mantenere l'iniezione della dipendenza ma avrò un design più intelligente che non richiede così tanta caldaia! grazie mille doc '
Dinaiz

4

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

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

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


DI può riguardare la leggibilità. Se hai una variabile globale "magicamente" che appare nel mezzo di un metodo, non aiuta a capire il codice (dal punto di vista della manutenzione). Da dove viene questa variabile? Chi lo possiede? Chi lo inizializza? E così via ... Per il tuo secondo punto, probabilmente hai ragione, ma non credo che si applichi nel mio caso. Grazie per aver
dedicato

DI aumenta la leggibilità principalmente perché "new / delete" verrà magicamente rimosso dal tuo codice. Se non devi preoccuparti del codice di allocazione dinamica è più facile da leggere.
GameDeveloper

Dimentica anche di dire che questo rende il codice un po 'più forte perché non può più fare ipotesi su "come" viene creata una dipendenza, "ne ha solo bisogno". E questo rende la classe più facile da mantenere .. vedi la mia risposta
GameDeveloper

3

Non sono sicuro del motivo per cui non vuoi rendere la tua fabbrica del tempo un singleton. Se ce n'è solo un'istanza nell'intera app, è di fatto un singleton.

Detto questo, è molto pericoloso condividere un oggetto mutabile, tranne se è adeguatamente protetto da blocchi di sincronizzazione, nel qual caso, non c'è motivo per non essere un singleton.

Se si desidera eseguire l'iniezione delle dipendenze, è possibile esaminare la primavera o altri framework di iniezione delle dipendenze, che consentono di assegnare automaticamente i parametri mediante un'annotazione


La primavera è per Java e io uso C ++. Ma perché 2 persone ti hanno votato? Ci sono punti nel tuo post con cui non sono d'accordo, ma comunque ...
Dinaiz,

Suppongo che il downvote non dovesse rispondere alla domanda di per sé, forse così come la menzione di Spring. Tuttavia, il suggerimento di utilizzare un Singleton era valido nella mia mente e potrebbe non rispondere alla domanda, ma suggerisce un'alternativa valida e solleva un interessante problema di progettazione. Per questo motivo ho +1.
Fergus A Londra,

1

Questo mira ad essere una risposta complementare a Doc Brown e anche a rispondere ai commenti senza risposta di Dinaiz che sono ancora correlati alla domanda.

Ciò di cui probabilmente hai bisogno è un framework per fare DI. Avere gerarchie complesse non significa necessariamente cattiva progettazione, ma se devi iniettare una TimeFactory dal basso (da A a D) invece di iniettare direttamente in D, probabilmente c'è qualcosa che non va nel modo in cui stai facendo l'iniezione di dipendenza.

Un singleton? No grazie. Se hai bisogno di una sola istanza per renderla condivisa nel contesto dell'applicazione (l'utilizzo di un contenitore IoC per DI come Infector ++ richiede solo di legare TimeFactory come istanza singola), ecco l'esempio (C ++ 11 a proposito, ma così C ++. Forse spostare su C ++ 11? Ottieni un'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 positivo di un contenitore IoC è che non è necessario passare il time factory a D. se la classe "D" richiede time factory, basta inserire time factory come parametro del 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 vedi iniettare TimeFactory solo una volta. Come usare "A"? Molto semplice, ogni classe viene iniettata, costruita principalmente o istanziata da 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 verrà automaticamente iniettato (istante pigro) iniettato con tutte le dipendenze fino a D e D verrà iniettato con TimeFactory, quindi chiamando solo 1 metodo avrai pronta la tua gerarchia completa (e anche le gerarchie complesse vengono risolte in questo modo rimozione di MOLTO codice della piastra della caldaia): non è necessario chiamare "nuovo / elimina" e questo è molto importante perché è possibile separare la logica dell'applicazione dal codice colla.

D può creare oggetti Time con informazioni che solo D può avere

È facile, il tuo TimeFactory ha un metodo "create", quindi usa una firma diversa "create (params)" e il gioco è fatto. I parametri che non sono dipendenze vengono spesso risolti in questo modo. Questo rimuove anche il dovere di iniettare cose come "stringhe" o "numeri interi" perché aggiungono solo una piastra aggiuntiva per la caldaia.

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 è necessario uno stato per le fabbriche). Puoi ancora usare le fabbriche come involucri per il contenitore IoC: in genere l'iniezione nel contenitore IoC è molto negativa ed è la stessa cosa che usare 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 contenitore e tutte le 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 dell'iniezione di dipendenza, i tipi semplici possono essere solo membri della classe o variabili con ambito locale. Questo sembra ovvio, ma ho visto la gente iniettare "std :: vector" solo perché c'era un framework DI che lo permetteva. Ricorda sempre la legge di Demetra: "Iniettare solo ciò di cui hai veramente bisogno per iniettare"


Wow, non ho visto la tua risposta prima, scusa! Signore molto istruttivo! Votato
Dinaiz il

0

Le tue classi A, B e C devono anche creare istanze Time o solo classe D? Se è solo classe D, A e B non dovrebbero sapere nulla di TimeFactory. Crea un'istanza di TimeFactory all'interno della classe C e passala alla classe D. Nota che per "creare un'istanza" non intendo necessariamente che la classe C debba essere responsabile dell'istanza di TimeFactory. Può ricevere DClassFactory dalla classe B e DClassFactory sa come creare un'istanza Time.

Una tecnica che uso spesso anche quando non ho un DI framework è fornire due costruttori, uno che accetta una factory e un altro che crea una factory di default. Il secondo ha generalmente un accesso protetto / pacchetto e viene utilizzato principalmente per i test unitari.


-1

Ho implementato ancora un altro framework di iniezione di dipendenza C ++, che recentemente è stato proposto per boost - https://github.com/krzysztof-jusiak/di - la libreria è meno macro (gratuita), solo intestazione, C ++ 03 / C ++ 11 / C ++ 14 che fornisce un tipo sicuro, tempo di compilazione, iniezione di dipendenza del costruttore senza macro.


3
Rispondere a vecchie domande su P.SE è un modo scadente per pubblicizzare il tuo progetto. Considera che ci sono decine di migliaia di progetti là fuori che possono avere una certa rilevanza per qualche domanda. Se tutti i loro autori lo pubblicassero qui, la qualità della risposta del sito precipiterebbe.
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.