Mettere in discussione uno degli argomenti per i framework di iniezione delle dipendenze: Perché è difficile creare un grafico a oggetti?


13

I framework di iniezione delle dipendenze come Google Guice danno la seguente motivazione per il loro utilizzo ( fonte ):

Per costruire un oggetto, devi prima costruirne le dipendenze. Ma per costruire ogni dipendenza, hai bisogno delle sue dipendenze e così via. Quindi quando costruisci un oggetto, devi davvero costruire un grafico a oggetti.

La creazione manuale di grafici a oggetti richiede molto lavoro (...) e rende difficili i test.

Ma non compro questo argomento: anche senza un framework di iniezione di dipendenza, posso scrivere classi che sono sia facili da istanziare che convenienti da testare. Ad esempio, l'esempio della pagina della motivazione di Guice potrebbe essere riscritto nel modo seguente:

class BillingService
{
    private final CreditCardProcessor processor;
    private final TransactionLog transactionLog;

    // constructor for tests, taking all collaborators as parameters
    BillingService(CreditCardProcessor processor, TransactionLog transactionLog)
    {
        this.processor = processor;
        this.transactionLog = transactionLog;
    }

    // constructor for production, calling the (productive) constructors of the collaborators
    public BillingService()
    {
        this(new PaypalCreditCardProcessor(), new DatabaseTransactionLog());
    }

    public Receipt chargeOrder(PizzaOrder order, CreditCard creditCard)
    {
        ...
    }
}

Quindi potrebbero esserci altri argomenti per i framework di iniezione delle dipendenze ( che non rientrano nell'ambito di questa domanda !), Ma la facile creazione di grafici di oggetti testabili non è uno di questi, vero?


1
Penso che un altro vantaggio dell'iniezione di dipendenza che viene spesso ignorato sia che ti costringe a sapere da cosa dipendono gli oggetti . Nulla appare magicamente negli oggetti, o si inietta con attributi esplicitamente contrassegnati o attraverso il costruttore direttamente. Per non parlare di come rende il tuo codice più testabile.
Benjamin Gruenbaum,

Nota che non sto cercando altri vantaggi dell'iniezione di dipendenza - Mi interessa solo capire l'argomento che "l'istanza di un oggetto è difficile senza iniezione di dipendenza"
oberlies

Questa risposta sembra fornire un ottimo esempio del motivo per cui vorresti una sorta di contenitore per i tuoi grafici degli oggetti (in breve, non stai pensando abbastanza in grande, usando solo 3 oggetti).
Izkata,

@Izkata No, non è una questione di dimensioni. Con l'approccio descritto, il codice di quel post sarebbe semplicemente new ShippingService().
oberlies,

@oberlies Still è; dovresti quindi andare a scavare in 9 definizioni di classe per capire quali classi vengono utilizzate per quel ShippingService, piuttosto che una posizione
Izkata,

Risposte:


12

C'è un vecchio, vecchio dibattito in corso sul modo migliore per fare l'iniezione di dipendenza.

  • Il taglio originale della molla ha creato un'istanza di un oggetto semplice, quindi ha iniettato dipendenze tramite i metodi setter.

  • Ma poi una grande contingenza di persone ha insistito sul fatto che l'iniezione di dipendenze attraverso i parametri del costruttore fosse il modo corretto di farlo.

  • Poi, ultimamente, quando l'uso della riflessione è diventato più comune, l'impostazione dei valori dei membri privati ​​direttamente, senza setter o argomenti del costruttore, è diventata la rabbia.

Quindi il tuo primo costruttore è coerente con il secondo approccio all'iniezione di dipendenza. Ti permette di fare cose carine come iniettare beffe per i test.

Ma il costruttore senza argomenti ha questo problema. Poiché sta creando un'istanza delle classi di implementazione per PaypalCreditCardProcessore DatabaseTransactionLog, crea una dipendenza dura, in fase di compilazione, da PayPal e dal database. Si assume la responsabilità di costruire e configurare correttamente l'intero albero delle dipendenze.

  • Immagina che il processore PayPay sia un sottosistema davvero complicato e integra molte librerie di supporto. Creando una dipendenza in fase di compilazione su quella classe di implementazione, si crea un collegamento infrangibile all'intero albero delle dipendenze. La complessità del grafico a oggetti è appena aumentata di un ordine di grandezza, forse due.

  • Molti di questi elementi nell'albero delle dipendenze saranno trasparenti, ma anche molti di essi dovranno essere istanziati. Le probabilità sono che non sarai in grado di creare un'istanza a PaypalCreditCardProcessor.

  • Oltre all'istanza, ciascuno degli oggetti richiederà proprietà applicate dalla configurazione.

Se si ha solo una dipendenza sull'interfaccia e si consente a una fabbrica esterna di costruire e iniettare la dipendenza, si interrompe l'intero albero delle dipendenze di PayPal e la complessità del codice si interrompe sull'interfaccia.

Ci sono altri vantaggi, come la specificazione delle classi di implementazione nella configurazione (cioè in fase di esecuzione anziché in fase di compilazione), o avere una specifica di dipendenza più dinamica che varia, diciamo, dall'ambiente (test, integrazione, produzione).

Ad esempio, supponiamo che PayPal Processor avesse 3 oggetti dipendenti e ognuna di queste dipendenze ne avesse altre due. E tutti quegli oggetti devono estrarre le proprietà dalla configurazione. Il codice così com'è dovrebbe assumersi la responsabilità di costruire tutto ciò, impostare le proprietà dalla configurazione, ecc. Ecc. - tutte le preoccupazioni di cui si occuperà il DI framework.

All'inizio potrebbe non sembrare ovvio da cosa ti stai proteggendo utilizzando un framework DI, ma si somma e diventa dolorosamente ovvio nel tempo. (lol parlo per esperienza di aver provato a farlo nel modo più duro)

...

In pratica, anche per un programma davvero minuscolo, trovo che finisco per scrivere in uno stile DI, e suddivido le classi in coppie di implementazione / fabbrica. Cioè, se non sto usando un framework DI come Spring, metto insieme alcune semplici classi di fabbrica.

Ciò fornisce la separazione delle preoccupazioni in modo che la mia classe possa fare solo le sue cose, e la classe di fabbrica si assume la responsabilità di costruire e configurare cose.

Non è un approccio richiesto, ma FWIW

...

Più in generale, il modello DI / interfaccia riduce la complessità del codice facendo due cose:

  • astrarre dipendenze a valle in interfacce

  • "sollevare" dipendenze a monte dal tuo codice e in una sorta di contenitore

Inoltre, poiché l'istanziazione e la configurazione degli oggetti sono un compito piuttosto familiare, il framework DI può raggiungere molte economie di scala attraverso la notazione standardizzata e l'uso di trucchi come la riflessione. Spargere quelle stesse preoccupazioni intorno alle lezioni finisce per aggiungere molto più disordine di quanto si possa pensare.


Il codice si assumerebbe la responsabilità di costruire tutto ciò - Una classe si assume solo la responsabilità di sapere quali sono le implementazioni dei suoi collaboratori. Non ha bisogno di sapere di cosa hanno bisogno questi collaboratori a loro volta. Quindi questo non è molto.
oberlies,

1
Ma se il costruttore PayPal Processor avesse 2 argomenti, da dove verrebbero?
Rob,

Non Proprio come BillingService, PayPalProcessor ha un costruttore zero-args che crea i collaboratori di cui ha bisogno.
oberlies il

astrarre dipendenze a valle in interfacce - Anche il codice di esempio fa questo. Il campo del processore è di tipo CreditCardProcessor e non del tipo di implementazione.
oberlies il

2
@oberlies: il tuo esempio di codice si trova a cavallo tra due stili, lo stile dell'argomento del costruttore e lo stile costruibile di zero-args. L'errore è che zero-args costruibile non è sempre possibile. La bellezza di DI o Factory è che in qualche modo permettono agli oggetti di essere costruiti con quello che sembra un costruttore zero-args.
rwong,

4
  1. Quando si nuota nella parte bassa della piscina, tutto è "facile e conveniente". Dopo aver superato una dozzina di oggetti, non è più conveniente.
  2. Nel tuo esempio, hai vincolato il tuo processo di fatturazione per sempre e un giorno a PayPal. Supponiamo di voler utilizzare un diverso processore di carte di credito? Supponiamo di voler creare un processore speciale per carte di credito vincolato alla rete? Oppure devi testare la gestione del numero di carta di credito? Hai creato un codice non portatile: "scrivi una volta, usa solo una volta perché dipende dal grafico specifico dell'oggetto per cui è stato progettato."

Associando il grafico a oggetti all'inizio del processo, ovvero collegandolo nel codice, è necessario che siano presenti sia il contratto sia l'implementazione. Se qualcun altro (forse anche tu) desidera utilizzare quel codice per uno scopo leggermente diverso, deve ricalcolare l'intero grafico dell'oggetto e reimplementarlo.

I framework DI ti consentono di prendere un sacco di componenti e collegarli insieme in fase di esecuzione. Ciò rende il sistema "modulare", composto da una serie di moduli che funzionano reciprocamente sulle interfacce anziché sulle reciproche implementazioni.


Quando seguo il tuo argomento, il ragionamento per DI avrebbe dovuto essere "la creazione manuale di un oggetto grafico è facile, ma porta a un codice che è difficile da mantenere". Tuttavia, l'affermazione era che "creare a mano un grafico a oggetti è difficile" e sto solo cercando argomenti a sostegno di tale affermazione. Quindi il tuo post non risponde alla mia domanda.
oberlies il

1
Immagino che se riduci la domanda alla "creazione di un oggetto grafico ...", allora è semplice. Il mio punto è che DI affronta il problema che non si affronta mai con un solo oggetto grafico; hai a che fare con una loro famiglia.
BobDalgleish,

In realtà, la creazione di un solo oggetto grafico può già essere complicata se i collaboratori sono condivisi .
oberlies,

4

L'approccio "un'istanza dei miei collaboratori" potrebbe funzionare per gli alberi delle dipendenze , ma certamente non funzionerà bene per i grafici delle dipendenze che sono grafici aciclici diretti (DAG). In un DAG di dipendenza, più nodi possono puntare allo stesso nodo, il che significa che due oggetti utilizzano lo stesso oggetto di collaboratore. In realtà, questo caso non può essere costruito con l'approccio descritto nella domanda.

Se alcuni dei miei collaboratori (o collaboratori del collaboratore) dovessero condividere un determinato oggetto, avrei bisogno di creare un'istanza di questo oggetto e passarlo ai miei collaboratori. Quindi, in effetti, avrei bisogno di sapere più dei miei collaboratori diretti, e questo ovviamente non si ridimensiona.


1
Ma l'albero delle dipendenze deve essere un albero, nel senso della teoria dei grafi. Altrimenti hai un ciclo, che è semplicemente insoddisfacente.
Xion,

1
@Xion Il grafico delle dipendenze deve essere un grafico aciclico diretto. Non è richiesto che esista un solo percorso tra due nodi come negli alberi.
oberlies il

@Xion: non necessariamente. Conside un UnitOfWorkche ha un DbContextrepository singolare e multiplo. Tutti questi repository dovrebbero usare lo stesso DbContextoggetto. Con la proposta "auto-istanziazione" di OP, ciò diventa impossibile da fare.
Flater,

1

Non ho usato Google Guice, ma ho impiegato molto tempo a migrare vecchie applicazioni legacy di livello N in .Net su architetture IoC come Onion Layer che dipendono da Dependency Injection per disaccoppiare le cose.

Perché l'iniezione di dipendenza?

Lo scopo di Dependency Injection non è in realtà per la testabilità, è in realtà prendere applicazioni strettamente accoppiate e allentare il giunto il più possibile. (Che ha il desiderabile per prodotto di rendere il tuo codice molto più facile da adattare per un corretto test dell'unità)

Perché dovrei preoccuparmi dell'accoppiamento?

L'accoppiamento o le dipendenze strette possono essere una cosa molto pericolosa. (Soprattutto nei linguaggi compilati) In questi casi potresti avere una libreria, una dll, ecc. Che viene usata molto raramente e che ha un problema che porta effettivamente l'intera applicazione offline. (Tutta la tua applicazione muore perché un pezzo non importante ha un problema ... questo è male ... DAVVERO cattivo) Ora, quando si disaccoppiano le cose, si può effettivamente configurare l'applicazione in modo che possa essere eseguita anche se quella DLL o libreria manca completamente! Sicuramente quel pezzo che ha bisogno di quella libreria o DLL non funzionerà, ma il resto dell'applicazione si accontenta come può essere.

Perché ho bisogno dell'iniezione di dipendenza per test adeguati

In realtà vuoi solo un codice liberamente accoppiato, l'iniezione di dipendenza lo abilita. Puoi accoppiare liberamente le cose senza IoC, ma in genere è più lavoro e meno adattabile (sono sicuro che qualcuno là fuori ha un'eccezione)

Nel caso in cui tu abbia dato, penso che sarebbe molto più semplice configurare solo l'iniezione di dipendenza, quindi posso prendere in giro il codice che non mi interessa contare come parte di questo test. Ti dico solo il metodo "Ehi, lo so che ti ho detto di chiamare il repository, ma invece ecco i dati che" dovrebbero "restituire l' occhiolino " ora perché quei dati non cambiano mai, sai che stai solo testando la parte che utilizza quei dati, non il recupero effettivo dei dati.

Fondamentalmente, quando si esegue il test, si desidera eseguire test di integrazione (funzionalità) che testano un pezzo di funzionalità dall'inizio alla fine, e un test completo dell'unità che testa ogni singolo pezzo di codice (in genere a livello di metodo o funzione) in modo indipendente.

L'idea è che vuoi assicurarti che l'intera funzionalità funzioni, se non vuoi conoscere l'esatto pezzo di codice che non funziona.

Questo può essere fatto senza Iniezione delle dipendenze, ma di solito man mano che il progetto cresce diventa sempre più complicato farlo senza Iniezione delle dipendenze. (Supponi SEMPRE che il tuo progetto crescerà! È meglio avere una pratica inutile di abilità utili che trovare un progetto che si avvia rapidamente e richiede un serio refactoring e reingegnerizzazione dopo che le cose stanno già decollando.)


1
Scrivere test usando una grande parte ma non tutto il grafico delle dipendenze è in realtà un buon argomento per DI.
oberlies,

1
Vale anche la pena notare che con DI puoi scambiare a livello di codice interi blocchi del tuo codice facilmente. Ho visto casi in cui un sistema avrebbe eliminato e reintegrato un'interfaccia con una classe completamente diversa per reagire a interruzioni e problemi di prestazioni da parte di servizi remoti. Potrebbe esserci stato un modo migliore per gestirlo, ma ha funzionato sorprendentemente bene.
RualStorge,

0

Come ho già detto in un'altra risposta , il problema qui è che si desidera che la classe Adipenda da una classeB senza codifica hard quale classe Bviene utilizzata nel codice sorgente di A. Questo è impossibile in Java e C # perché l'unico modo per importare una classe è fare riferimento ad esso con un nome univoco globale.

Usando un'interfaccia puoi aggirare la dipendenza di classe hardcoded, ma devi ancora mettere le mani su un'istanza dell'interfaccia e non puoi chiamare i costruttori o sei di nuovo nel quadrato 1. Quindi ora il codice che altrimenti potrebbe creare le sue dipendenze spinge quella responsabilità verso qualcun altro. E le sue dipendenze stanno facendo la stessa cosa. Quindi ora ogni volta che hai bisogno di un'istanza di una classe finisci per costruire manualmente l'intero albero delle dipendenze, mentre nel caso in cui la classe A dipenda direttamente da B potresti semplicemente chiamare new A()e avere quella chiamata del costruttore new B(), e così via.

Un framework di iniezione delle dipendenze tenta di aggirare ciò consentendoti di specificare i mapping tra le classi e costruendo l'albero delle dipendenze per te. Il problema è che quando si rovinano i mapping, lo scoprirai in fase di esecuzione, non in fase di compilazione come faresti in linguaggi che supportano i moduli di mapping come concetto di prima classe.


0

Penso che questo sia un grande fraintendimento qui.

Guice è un framework di iniezione di dipendenza . Rende DI automatico . Il punto che hanno sottolineato nell'estratto che hai citato riguarda il fatto che Guice è in grado di rimuovere la necessità di creare manualmente quel "costruttore verificabile" che hai presentato nel tuo esempio. Non ha assolutamente nulla a che fare con l'iniezione di dipendenza stessa.

Questo costruttore:

BillingService(CreditCardProcessor processor, TransactionLog transactionLog)
{
    this.processor = processor;
    this.transactionLog = transactionLog;
}

utilizza già l'iniezione di dipendenza. Fondamentalmente hai appena detto che usare DI è facile.

Il problema che Guice risolve è che per usare quel costruttore ora devi avere un codice costruttore di oggetti grafico da qualche parte , passando manualmente gli oggetti già istanziati come argomenti di quel costruttore. Guice ti consente di avere un unico posto dove puoi configurare quali classi di implementazione reali corrispondono a quelle CreditCardProcessore TransactionLoginterfacce. Dopo quella configurazione, ogni volta che crei BillingServiceusando Guice, quelle classi saranno passate automaticamente al costruttore.

Questo è ciò che fa il framework di iniezione delle dipendenze . Ma il costruttore stesso che hai presentato è già un'implementazione del principio dell'iniezione di depencency . I contenitori IoC e i framework DI sono mezzi per automatizzare i principi corrispondenti, ma non c'è nulla che ti impedisca di fare tutto a mano, questo è il punto.

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.