Quali sono i vantaggi dell'iniezione di dipendenza nei casi in cui quasi tutti hanno bisogno di accedere a una struttura dati comune?


20

Ci sono molte ragioni per cui i globi sono cattivi in OOP.

Se il numero o la dimensione degli oggetti che richiedono la condivisione è troppo grande per essere passati in modo efficiente nei parametri di funzione, di solito tutti raccomandano l' iniezione di dipendenza anziché un oggetto globale.

Tuttavia, nel caso in cui quasi tutti debbano conoscere una determinata struttura di dati, perché Dependency Injection è meglio di un oggetto globale?

Esempio (uno semplificato, per mostrare il punto in generale, senza approfondire troppo in una specifica applicazione)

Esiste un numero di veicoli virtuali che hanno un numero enorme di proprietà e stati, da tipo, nome, colore, velocità, posizione, ecc. Un numero di utenti può controllarli a distanza e un numero enorme di eventi (entrambi gli utenti- avviato e automatico) possono cambiare molti dei loro stati o proprietà.

La soluzione ingenua sarebbe quella di crearne un contenitore globale, come

vector<Vehicle> vehicles;

a cui è possibile accedere da qualsiasi luogo.

La soluzione più intuitiva di OOP sarebbe quella di avere il contenitore membro della classe che gestisce il ciclo di eventi principale e di essere istanziato nel suo costruttore. Ogni classe che ne ha bisogno ed è membro del thread principale, avrà accesso al contenitore tramite un puntatore nel proprio costruttore. Ad esempio, se un messaggio esterno arriva tramite una connessione di rete, subentra una classe (una per ogni connessione) che gestisce l'analisi e il parser avrà accesso al contenitore tramite un puntatore o un riferimento. Ora, se il messaggio analizzato provoca una modifica in un elemento del contenitore o richiede alcuni dati al di fuori di esso per eseguire un'azione, può essere gestito senza la necessità di lanciare migliaia di variabili attraverso segnali e slot (o peggio, memorizzandoli nel parser per essere successivamente recuperati da chi ha chiamato il parser). Naturalmente, tutte le classi che ricevono l'accesso al contenitore tramite iniezione di dipendenza, fanno parte dello stesso thread. Diversi thread non accederanno direttamente ad esso, ma faranno il loro lavoro e quindi invieranno segnali al thread principale, e gli slot nel thread principale aggiorneranno il contenitore.

Tuttavia, se la maggior parte delle classi avrà accesso al contenitore, cosa lo rende davvero diverso da un globale? Se così tante classi hanno bisogno dei dati nel contenitore, il "modo di iniezione di dipendenza" non è solo un globale mascherato?

Una risposta sarebbe la sicurezza del thread: anche se mi preoccupo di non abusare del container globale, forse un altro sviluppatore in futuro, sotto la pressione di una scadenza ravvicinata, utilizzerà comunque il container globale in un thread diverso, senza occuparmi di tutto i casi di collisione. Tuttavia, anche in caso di iniezione di dipendenza, si potrebbe dare un puntatore a qualcuno che gira in un altro thread, portando agli stessi problemi.


6
Aspetta, parli di globi non mutabili e usi un link a "perché lo stato globale è cattivo" per giustificarlo? WTF?
Telastyn,

1
Non sto affatto giustificando i globali. Penso che sia chiaro che sono d'accordo con il fatto che i globi non sono la buona soluzione. Sto solo parlando di una situazione in cui anche l'uso dell'iniezione di dipendenza non può essere molto migliore di (o forse anche quasi indistinguibile da) i globi. Quindi una risposta potrebbe indicare altri vantaggi nascosti dell'utilizzo dell'iniezione di dipendenza in questa situazione, o che altri approcci sarebbero migliori del di
vsz

9
Ma c'è una netta differenza tra i globuli di sola lettura e lo stato globale.
Telastyn,


1
@vsz non proprio - la domanda riguarda le fabbriche di localizzatori di servizi come un modo per aggirare il problema di molti oggetti distinti; ma le sue risposte rispondono anche alla tua domanda di consentire alle classi di accedere ai dati globali anziché ai dati globali che vengono passati alle classi.
gbjbaanb,

Risposte:


37

nel caso in cui quasi tutti debbano conoscere una determinata struttura di dati, perché Dependency Injection è meglio di un oggetto globale?

L'iniezione di dipendenza è la cosa migliore dopo il pane a fette , mentre gli oggetti globali sono noti da decenni come la fonte di tutto il male , quindi questa è una domanda piuttosto interessante.

Il punto dell'iniezione di dipendenza non è semplicemente quello di garantire che ogni attore che ha bisogno di alcune risorse possa averlo, perché ovviamente, se rendi globali tutte le risorse, allora ogni attore avrà accesso a ogni risorsa, problema risolto, giusto?

Il punto di iniezione di dipendenza è:

  1. Per consentire agli attori di accedere alle risorse in base alle necessità , e
  2. Per avere il controllo su quale istanza di una risorsa è accessibile da un determinato attore.

È irrilevante il fatto che nella tua particolare configurazione tutti gli attori debbano accedere alla stessa istanza di risorsa. Fidati di me, un giorno avrai la necessità di riconfigurare le cose in modo che gli attori abbiano accesso a diverse istanze della risorsa, e poi ti accorgerai di esserti dipinto in un angolo. Alcune risposte hanno già indicato una tale configurazione: test .

Un altro esempio: supponiamo di dividere l'applicazione in client-server. Tutti gli attori sul client utilizzano lo stesso insieme di risorse centrali sul client e tutti gli attori sul server utilizzano lo stesso insieme di risorse centrali sul server. Supponiamo ora, un giorno, che decidi di creare una versione "autonoma" dell'applicazione client-server, in cui sia il client che il server sono impacchettati in un singolo eseguibile e in esecuzione nella stessa macchina virtuale. (O ambiente di runtime, a seconda della lingua scelta.)

Se si utilizza l'inserimento delle dipendenze, è possibile assicurarsi facilmente che tutti gli attori client ricevano le istanze delle risorse client con cui lavorare, mentre tutti gli attori server ricevono le istanze delle risorse server.

Se non si utilizza l'iniezione di dipendenza, si è completamente sfortunati, poiché in una macchina virtuale può esistere solo un'istanza globale di ciascuna risorsa.

Quindi, devi considerare: tutti gli attori hanno davvero bisogno di accedere a quella risorsa? veramente?

È possibile che tu abbia commesso l'errore di trasformare quella risorsa in un oggetto divino (quindi, ovviamente, tutti hanno bisogno di accedervi) o forse stai sovrastimando gravemente il numero di attori nel tuo progetto che hanno effettivamente bisogno di accedere a quella risorsa .

Con i globali, ogni singola riga di codice sorgente nell'intera applicazione ha accesso a ogni singola risorsa globale. Con l'iniezione di dipendenza, ogni istanza di risorsa è visibile solo agli attori che ne hanno effettivamente bisogno. Se i due sono gli stessi (gli attori che necessitano di una particolare risorsa comprendono il 100% delle righe del codice sorgente nel progetto), allora devi aver commesso un errore nel tuo design. Quindi, neanche

  • Ricalcola quella grande e grande risorsa divina in piccole risorse secondarie, quindi attori diversi hanno bisogno di accedere a pezzi diversi di esso, ma raramente un attore ha bisogno di tutti i suoi pezzi, o

  • Rifattorizza i tuoi attori affinché a loro volta accettino come parametri solo i sottoinsiemi del problema su cui devono lavorare, quindi non devono sempre consultare alcune grandi risorse centrali.


Non sono sicuro di capirlo - da un lato dici che DI ti consente di usare più istanze di una risorsa e i globali no, il che è chiaramente una sciocchezza a meno che tu non stia confondendo una singola variabile statica con oggetti con istanze multiple - non c'è motivo che "il globale" deve essere una singola istanza o una singola classe.
gbjbaanb,

3
@gbjbaanb Supponiamo che la risorsa sia uno Zork. L'OP sta dicendo che non solo ogni singolo oggetto nel suo sistema ha bisogno di uno Zork con cui lavorare, ma anche che esisterà un solo Zork e che tutti i suoi oggetti avranno solo accesso a quell'unica istanza di Zork. Sto dicendo che nessuna di queste due assunzioni è ragionevole: probabilmente non è vero che tutti i suoi oggetti hanno bisogno di uno Zork, e ci saranno momenti in cui alcuni degli oggetti avranno bisogno di una certa istanza di Zork, mentre altri oggetti avranno bisogno di un diversa istanza di Zork.
Mike Nakis,

2
@gbjbaanb Suppongo che non mi stia chiedendo di spiegare perché sarebbe stupido avere due istanze globali di Zork, chiamarle ZorkA e ZorkB e codificare negli oggetti quale uno utilizzerà ZorkA e quale utilizzerà ZorkB, giusto?
Mike Nakis,

1
Non ho detto tutti, ho detto quasi tutti. Il punto della domanda era se DI avesse altri vantaggi oltre a negare l'accesso a quei pochi attori che non ne avevano bisogno. So che gli "oggetti divini" sono un anti-modello, ma anche seguire ciecamente le migliori pratiche può esserlo. Può succedere che lo scopo di un programma sia quello di fare varie cose con una determinata risorsa, in quel caso quasi tutti hanno bisogno di accedere a quella risorsa. Un buon esempio potrebbe essere un programma che lavora su un'immagine: quasi tutto ciò che ha a che fare ha qualcosa a che fare con i dati dell'immagine.
vsz

1
@gbjbaanb Credo che l'idea sia quella di avere una classe client in grado di lavorare esclusivamente su ZorkA o ZorkB come indicato, senza che la classe client decida quale di loro prendere.
Eugene Ryabtsev,

39

Ci sono molte ragioni per cui i globuli non mutabili sono cattivi in ​​OOP.

Questa è un'affermazione dubbia. Il collegamento si utilizza come prova si riferisce alla statale - mutabile globali. Sono decisamente malvagi. I globuli di sola lettura sono solo costanti. Le costanti sono relativamente sane. Voglio dire, non inietterai il valore di pi in tutte le tue classi, vero?

Se il numero o la dimensione degli oggetti che richiedono la condivisione è troppo grande per essere passati in modo efficiente nei parametri di funzione, di solito tutti raccomandano l'iniezione di dipendenza anziché un oggetto globale.

No, non lo fanno.

Se le tue funzioni / classi hanno troppe dipendenze, le persone consigliano di fermarsi e dare un'occhiata al motivo per cui il tuo design è così male. Avere un carico di dipendenze è un segno che la tua classe / funzione sta probabilmente facendo troppe cose e / o non hai un'astrazione sufficiente.

Esiste un numero di veicoli virtuali che hanno un numero enorme di proprietà e stati, da tipo, nome, colore, velocità, posizione, ecc. Un numero di utenti può controllarli a distanza e un numero enorme di eventi (entrambi gli utenti- avviato e automatico) possono cambiare molti dei loro stati o proprietà.

Questo è un incubo di design. Hai un sacco di cose che possono essere usate / abusate senza alcun modo di convalida, nessun modo di limiti di concorrenza - è solo un accoppiamento dappertutto.

Tuttavia, se la maggior parte delle classi avrà accesso al contenitore, cosa lo rende davvero diverso da un globale? Se così tante classi hanno bisogno dei dati nel contenitore, il "modo di iniezione di dipendenza" non è solo un globale mascherato?

Sì, avere l'accoppiamento con dati comuni ovunque non è troppo diverso da un globale, e come tale, ancora male.

Il lato positivo è che stai disaccoppiando i consumatori dalla stessa variabile globale. Ciò fornisce una certa flessibilità nel modo in cui viene creata l'istanza. Inoltre non ti obbliga ad avere solo un'istanza. Potresti farne passare diversi tra i tuoi diversi consumatori. È inoltre possibile creare diverse implementazioni dell'interfaccia per ogni consumatore nel caso fosse necessario.

E a causa dei cambiamenti che inevitabilmente derivano dalle mutevoli esigenze del software, un tale livello di flessibilità di base è vitale .


scusate la confusione con "non mutevole", volevo dire mutevole e in qualche modo non ho riconosciuto il mio errore.
vsz,

"Questo è un incubo di design" - lo so. Ma se i requisiti sono che tutti quegli eventi devono essere in grado di eseguire le modifiche ai dati, allora gli eventi saranno accoppiati ai dati anche se li divido e li nascondo sotto uno strato di astrazioni. L'intero programma riguarda questi dati. Ad esempio, se un programma deve eseguire molte diverse operazioni su un'immagine, allora tutte o quasi tutte le sue classi saranno in qualche modo accoppiate ai dati dell'immagine. Il solo dire "scriviamo un programma che fa qualcosa di diverso" non è accettabile.
vsz,

2
Le applicazioni che hanno molti oggetti che accedono abitualmente ad alcuni database non sono esattamente senza precedenti.
Robert Harvey,

@RobertHarvey - certo, ma l'interfaccia da cui dipendono "può accedere a un database" o "un archivio dati persistente per foos"?
Telastyn,

1
Mi ricorda un Dilbert in cui PHB chiede 500 funzionalità e Dilbert dice di no. Quindi PHB richiede 1 funzione e Dilbert dice di sicuro. Il giorno successivo Dilbert riceve 500 richieste ciascuna per 1 funzione. Se qualcosa non si ridimensiona, non si ridimensiona, non importa come lo vesti.
corsiKa

16

Ci sono tre ragioni principali che dovresti considerare.

  1. Leggibilità. Se ogni unità di codice ha tutto ciò di cui ha bisogno per funzionare sia iniettata o passata come parametro, è facile guardare il codice e vedere immediatamente cosa sta facendo. Questo ti dà una località di funzione, che ti consente anche di separare meglio le preoccupazioni e ti costringe anche a pensare ...
  2. Modularità. Perché il parser dovrebbe conoscere l'intero elenco di veicoli e tutte le loro proprietà? Probabilmente no. Forse deve interrogare se esiste un ID veicolo o se il veicolo X ha la proprietà Y. Ottimo, ciò significa che puoi scrivere un servizio che lo fa e iniettare solo quel servizio nel tuo parser. All'improvviso si finisce con un design che ha molto più senso, con ogni bit di codice che gestisce solo i dati che sono rilevanti per esso. Il che ci porta a ...
  3. Testabilità. Per un tipico test unitario si desidera impostare il proprio ambiente per diversi scenari e l'iniezione lo rende molto facile da implementare. Ancora una volta, tornando all'esempio del parser, vuoi davvero creare sempre a mano un intero elenco di veicoli completi per ogni caso di test che scrivi per il tuo parser? O creeresti semplicemente un'implementazione simulata del suddetto oggetto di servizio, restituendo un numero variabile di ID? So quale sceglierei.

Quindi, per riassumere: DI non è un obiettivo, è solo uno strumento che consente di raggiungere un obiettivo. Se lo usi in modo improprio (come sembra fare nel tuo esempio), non ti avvicinerai più all'obiettivo, non c'è alcuna proprietà magica di DI che renderà buono un cattivo design.


+1 per una risposta concisa incentrata sui problemi di manutenzione. Altre risposte P: SE dovrebbero essere come questa.
dodgethesteamroller,

1
Il tuo riassunto è così buono. Davvero buono. Se pensi che [modello di progettazione del mese] o [parola d'ordine del mese] risolveranno magicamente i tuoi problemi, avrai un brutto momento.
corsiKa

@corsiKa Sono stato contrario a DI (o Spring, per essere più precisi) per molto tempo perché non riuscivo a capirne il punto. Sembrava davvero una seccatura senza problemi tangibili (questo era nei giorni della configurazione XML). Vorrei aver trovato della documentazione su ciò che DI effettivamente ti compra allora. Ma non l'ho fatto, quindi ho dovuto capirlo da solo. Ci è voluto molto tempo. :)
biziclop,

3

Riguarda davvero la testabilità: normalmente un oggetto globale è molto gestibile e di facile lettura / comprensione (una volta che lo sai lì, ovviamente) ma è un dispositivo permanente e quando vieni a dividere le tue classi in test isolati, scopri che il tuo l'oggetto globale è bloccato a dire "che dire di me" e quindi i tuoi test isolati richiedono che l'oggetto globale sia incluso in essi. Non aiuta il fatto che la maggior parte dei framework di test non supporti facilmente questo scenario.

Quindi sì, è ancora globale. Il motivo fondamentale per averlo non è cambiato, solo il modo in cui si accede.

Per quanto riguarda l'inizializzazione, questo è ancora un problema: devi ancora impostare la configurazione in modo che i test possano essere eseguiti, quindi non stai ottenendo nulla lì. Suppongo che potresti dividere un grande oggetto dipendente in molti più piccoli e passare quello appropriato alle classi che ne hanno bisogno, ma probabilmente stai diventando ancora più complessa per superare qualsiasi beneficio.

Indipendentemente da tutto ciò, è un esempio di "coda che scuote il cane", la necessità di testare sta guidando il modo in cui la base di codice è progettata (problemi simili sorgono con elementi come le classi statiche, ad esempio il datetime attuale).

Personalmente preferisco inserire tutti i miei dati globali in un oggetto globale (o diversi), ma fornire gli accessor per ottenere i bit di cui le mie classi hanno bisogno. Quindi posso prendere in giro quelli per restituire i dati che voglio quando si tratta di test senza la complessità aggiunta di DI.


"normalmente un oggetto globale è molto gestibile" - non se è mutabile. Nella mia esperienza, avere uno stato globale così mutevole può essere un incubo (anche se ci sono modi per renderlo sopportabile).
sleske,

3

Oltre alle risposte che hai già,

Esiste un numero di veicoli virtuali che hanno un numero enorme di proprietà e stati, da tipo, nome, colore, velocità, posizione, ecc. Un numero di utenti può controllarli a distanza e un numero enorme di eventi (entrambi gli utenti- avviato e automatico) possono cambiare molti dei loro stati o proprietà.

Una delle caratteristiche della programmazione OOP è quella di mantenere solo le proprietà realmente necessarie per un oggetto specifico,

ORA SE il tuo oggetto ha troppe proprietà - dovresti suddividere ulteriormente l'oggetto in sotto-oggetti.

modificare

Ho già detto perché gli oggetti globali sono cattivi, aggiungerei un esempio.

È necessario eseguire test di unità sul codice GUI di un modulo di Windows, se si utilizza globale, sarà necessario creare tutto il pulsante ed è eventi ad esempio per creare un caso di test adeguato ...


Vero, ma ad esempio, al parser verrà richiesto di conoscere tutto a causa di qualsiasi comando che può essere ricevuto e che può apportare modifiche a qualsiasi cosa.
vsz,

1
"Prima di venire alla tua vera domanda" ... hai intenzione di rispondere alla sua domanda?
gbjbaanb,

La domanda di @gbjbaanb ha molti messaggi, per favore concedimi un po 'di tempo per leggerlo correttamente
Matematica
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.