Interrogazione di uno degli argomenti per l'iniezione delle dipendenze: Perché la creazione di un oggetto grafico è difficile?

7

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

To construct an object, you first build its dependencies. But to build each dependency, you need its dependencies, and so on. So when you build an object, you really need to build an object graph.

Building object graphs by hand is labour intensive (...) and makes testing difficult.

Ma non compro questo argomento: anche senza l'iniezione di dipendenza, posso scrivere classi che sono sia facili da istanziare che convenienti da testare. Per esempio. l'esempio della pagina di 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 l'iniezione delle dipendenze ( che non rientrano nell'ambito di questa domanda !), ma la creazione semplice di grafici di oggetti testabili non è uno di questi, vero?

    
posta oberlies 21.08.2014 - 18:58
fonte

5 risposte

3

L'approccio "istanziare i miei collaboratori" potrebbe funzionare per gli alberi alberi di dipendenza, ma sicuramente non funzionerà bene per i grafici delle dipendenze che sono grafi 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 un collaboratore. Questo caso non può in effetti essere costruito con l'approccio descritto nella domanda.

Se alcuni dei miei collaboratori (o collaboratori collaboratori) dovessero condividere un determinato oggetto, avrei bisogno di istanziare questo oggetto e di trasmetterlo ai miei collaboratori. Quindi, in realtà, avrei bisogno di sapere più dei miei diretti collaboratori, e questo ovviamente non è in scala.

    
risposta data 21.08.2014 - 19:30
fonte
8

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

  • Il taglio originale di primavera creava un'istanza di un oggetto semplice, quindi le dipendeva dall'iniezione tramite i metodi setter.

  • Ma poi una grande contingenza di persone ha insistito sul fatto che iniettare dipendenze attraverso i parametri del costruttore era il modo corretto per farlo.

  • Quindi, ultimamente, quando l'uso del reflection è diventato più comune, l'impostazione dei valori dei membri privati direttamente, senza setter o argomenti del costruttore, è diventata di dominio.

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

Ma il costruttore senza argomenti ha questo problema. Poiché crea un'istanza delle classi di implementazione per PaypalCreditCardProcessor e DatabaseTransactionLog , crea una dipendenza dura e in fase di compilazione su PayPal e il Database. Si assume la responsabilità della creazione e della configurazione dell'intero albero delle dipendenze in modo corretto.

  • Immagina che il processore PayPay sia un sottosistema davvero complicato e che aggiunga anche molte librerie di supporto. Creando una dipendenza in fase di compilazione su tale classe di implementazione, si sta creando un collegamento indissolubile all'intero albero delle dipendenze. La complessità del tuo oggetto grafico è appena aumentata di un ordine di grandezza, forse di due.

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

  • Oltre all'istanza, ciascuno degli oggetti avrà bisogno di proprietà applicate dalla configurazione.

Se si ha solo una dipendenza dall'interfaccia e si consente a un factory esterno di costruire e iniettare la dipendenza, si taglia l'intero albero delle dipendenze di PayPal e la complessità del codice si interrompe nell'interfaccia.

Ci sono altri vantaggi, come quello di specificare le classi di implementazioni in configurazione (ad esempio in fase di esecuzione piuttosto che in fase di compilazione) o con più specifiche di dipendenza dinamica che variano, ad esempio, per ambiente (test, integrazione, produzione).

Ad esempio, supponiamo che PayPalProcessor abbia 3 oggetti dipendenti e ognuna di queste dipendenze ne abbia altre due. E tutti quegli oggetti devono inserire proprietà dalla configurazione. Il codice così come è, si assumerebbe la responsabilità di costruire tutto questo, impostando le proprietà dalla configurazione, ecc. Ecc. - tutte le preoccupazioni che il framework DI si prenderà cura di noi.

Potrebbe non sembrare ovvio in un primo momento da cosa ti stai proteggendo usando un framework DI, ma si aggiunge e diventa dolorosamente ovvio nel tempo. (lol parlo per l'esperienza di aver provato a farlo nel modo più duro)

...

In pratica, anche per un programma veramente piccolo, trovo che finisco per scrivere in uno stile DI e suddividere le classi in coppie implementazione / factory. Cioè, se non sto usando un framework DI come Spring, mi limito a mettere insieme alcune semplici classi di factory.

Ciò fornisce la separazione delle preoccupazioni in modo che la mia classe possa semplicemente fare ciò che è, e la classe factory si assume la responsabilità di costruire & configurazione delle cose.

Non un approccio obbligatorio, ma FWIW

...

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

  • astrazione delle dipendenze downstream nelle interfacce

  • "sollevamento" delle dipendenze upstream dal tuo codice e in una sorta di contenitore

Inoltre, poiché l'istanziazione degli oggetti e la configurazione sono un compito abbastanza familiare, il framework DI può ottenere molte economie di scala attraverso la notazione e l'elaborazione standardizzate. usando trucchi come la riflessione. Scattering quelle stesse preoccupazioni intorno alle classi finisce per aggiungere molto più confusione di quanto si potrebbe pensare.

    
risposta data 21.08.2014 - 19:14
fonte
4
  1. Quando nuotate nella parte bassa della piscina, tutto è "facile e conveniente". Una volta superata 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. Supponi di voler utilizzare un diverso processore per carte di credito? Si supponga di voler creare un processore di carte di credito specializzato vincolato alla rete? O è necessario testare la gestione del numero di carta di credito? Hai creato un codice non portatile: "scrivi una volta, usa una sola volta perché dipende dal grafico dell'oggetto specifico per il quale è stato progettato."

Collegando il tuo oggetto grafico all'inizio del processo, cioè, collegandolo con il codice, richiedi che sia presente sia il contratto che l'implementazione. Se qualcun altro (forse anche tu) vuole usare quel codice per uno scopo leggermente diverso, deve ricalcolare l'intero grafico dell'oggetto e reimplementarlo.

I framework DI consentono di prendere un sacco di componenti e collegarli in fase di runtime. Ciò rende il sistema "modulare", composto da un numero di moduli che funzionano con le interfacce degli altri invece che con le implementazioni degli altri.

    
risposta data 21.08.2014 - 19:15
fonte
1

Non ho usato Google Guice, ma ho impiegato molto tempo per la migrazione di vecchie applicazioni legacy N-tier in .Net a architetture IoC come Onion Layer che dipendono dalla dipendenza dall'iniezione per disaccoppiare le cose.

Why Dependency Injection?

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

Perché dovrei preoccuparmi dell'accoppiamento?

L'accoppiamento o le dipendenze strette possono essere una cosa molto pericolosa. (Soprattutto in lingue compilate) In questi casi è possibile avere una libreria, una DLL, ecc. Che viene utilizzata molto raramente e che presenta 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 male) Ora, quando si disaccoppiano le cose, è possibile configurare l'applicazione in modo che possa essere eseguita anche se la DLL o la libreria sono completamente mancanti! Certo che un pezzo che ha bisogno di quella libreria o DLL non funzionerà, ma il resto delle applicazioni sembra felice come può essere.

Perché ho bisogno di Iniezione delle dipendenze per i test appropriati

In realtà si desidera semplicemente un codice liberamente accoppiato, la Dependency Injection lo consente. Puoi accoppiare le cose senza I / O, ma in genere è più lavoro e meno adattabile (sono sicuro che qualcuno là fuori ha un'eccezione)

Nel caso in cui mi hai dato, penso che sarebbe molto più semplice impostare l'iniezione delle dipendenze in modo da poter simulare il codice. Non sono interessato a contare come parte di questo test. Ti dico solo il metodo "Ehi, so che ti ho detto di chiamare il repository, ma qui i dati devono" restituire strizzatine d'occhio "ora perché i dati non cambiano mai sai che stai solo testando la parte che usa quei dati, non il reale recupero dei dati.

Fondamentalmente, quando si eseguono i test, si vogliono test di integrazione (funzionalità) che testano una funzionalità dall'inizio alla fine e test unitari completi che testano ogni singolo codice (in genere a livello di metodo o di funzione) indipendentemente.

L'idea è che tu voglia essere sicuro che l'intera funzionalità funzioni, altrimenti non vuoi sapere l'esatto codice del codice che non funziona.

Questo CAN può essere fatto senza Iniezione di Dipendenza, ma di solito quando il progetto cresce, diventa sempre più ingombrante farlo senza Iniezione di Dipendenza. (Supponete SEMPRE che il vostro progetto cresca! È meglio avere pratica inutile di abilità utili piuttosto che trovare un progetto che si avvia rapidamente e richiede un serio refactoring e reingegnerizzazione dopo che le cose stanno già decollando.)

    
risposta data 21.08.2014 - 19:22
fonte
0

Come menziono in un'altra risposta , il problema qui è che vuoi che la classe A dipenda da qualche classe B senza codifica hard quale classe B è usata nel codice sorgente di A. Questo è impossibile in Java e C # perché l'unico modo per importare una classe deve fare riferimento ad essa con un nome univoco globale.

Usando un'interfaccia puoi aggirare la dipendenza della classe hard-coded, ma devi comunque mettere le mani su un'istanza dell'interfaccia, e non puoi chiamare costruttori o sei di nuovo nel quadrato 1. Così ora il codice che potrebbe altrimenti creare le sue dipendenze spinge questa responsabilità a qualcun altro. E le sue dipendenze stanno facendo la stessa cosa. Quindi ora ogni tempo hai bisogno di un'istanza di una classe che finisci per costruire manualmente l'intero albero delle dipendenze, mentre nel caso in cui la classe A dipende direttamente da B puoi semplicemente chiamare new A() e avere quel costruttore chiama new B() e così via.

Un framework di iniezione delle dipendenze tenta di aggirare il problema consentendo di specificare i mapping tra le classi e di costruire l'albero delle dipendenze. Il problema è che quando svaliggi i mapping, lo scoprirai in fase di esecuzione, non in fase di compilazione come faresti in lingue che supportano i moduli di mappatura come un concetto di prima classe.

    
risposta data 21.08.2014 - 19:28
fonte