Come sapere quando usare l'iniezione di dipendenza?

7

La domanda potrebbe sembrare un po 'strana, e credo lo sia. Mi è venuta la domanda durante la navigazione attraverso alcuni modelli di progettazione. Sono arrivato al famoso schema di stato / strategia e sono arrivato a questo codice:

public class Account
{
    public double Balance { get; private set; }

    private IAccountState state;

    public Account()
    {
        state = new ActiveAccountState();
    }

    public void Deposit(double number)
    {
        state = state.Deposit(() => Balance += number);
    }

    public void Withdraw(double number)
    {
        state = state.Withdraw(() => Balance -= number);
    }

    public void Freeze()
    {
        state = state.Freeze();
    }

    public void Close()
    {
        state = state.Close();
    }
}

È semplicistico e non è già pronto per la produzione, ma non sto cercando di farlo. È un semplice esempio Questo codice mostra effettivamente il problema che sto avendo. Fondamentalmente ho un'interfaccia IAccountState che rappresenta lo stato / strategia. Ora questa può essere considerata una dipendenza e consentendo alla classe Account di SAPERE come produrre ActiveAccountState. Permetto alla classe Account di avere più di 1 responsabilità. Potrei creare un accountStateFactory e farne una dipendenza dalla classe di account. Ciò mi permetterebbe di iniettare facilmente questa dipendenza Factory appena creata con l'uso del costruttore della classe Account. In questo modo, tuttavia, espongo i dettagli di implementazione che sono probabilmente solo interessanti per la classe Account e per nessun altro. Immagino che renderebbe il test delle unità un po 'più semplice.

La mia domanda sarebbe come sapere se qualcosa dovrebbe essere una dipendenza? A volte è semplice (servizi, fabbriche, fornitori, ecc.) Ma ci sono delle volte in cui sono confuso (come in questo esempio di stato / strategia). Grazie in anticipo!

    
posta user3357969 09.06.2018 - 22:10
fonte

6 risposte

18

how to know if something should be a dependency?

Ogni volta che hai bisogno di qualcosa, ma non vuoi farlo qui.

La cosa importante che vorresti che tu capisca del reference passing (o Dependency Injection se insisti ad essere fantasioso) è che usare un buon default è ok. Non ti è proibito usare nuovi. Vi è vietato utilizzare il nuovo senza fornire un modo per ignorare ciò che vi fa notare.

Questo è scarsamente comprensibile in linguaggi (come Java) che non hanno parametri con nome. C # li ha in modo che voi ragazzi non abbiate scuse. Josh Bloch ha fornito agli utenti Java il suo modello di builder per risolvere il problema, quindi se lo sai , non hai nemmeno scuse.

I parametri con nome rendono facile avere argomenti opzionali. Se lo hai, puoi avere facilmente i valori predefiniti. Il tuo stato dovrebbe essere un valore predefinito sovrascrivibile. Questo ti permette di usare DI senza soffrire con classi indifese che non possono costruirsi da sole.

La filosofia dietro questo approccio diverso alla DI è chiamata convenzione sulla configurazione . Provalo. Scommetto che scoprirai che rende queste idee molto meno teoriche e più utili.

Ci sono altri problemi che non hai chiesto e dal momento che non si tratta di Revisione del codice non entrerò tutti loro, ma non posso ignorare il double Balance uno. Per favore dimmi che non rappresenta dollari e centesimi. Double funziona bene su pollici discreti (base 2) e misure non discrete / continue (senza base) ma non sulla base 10 valute. I punti di galleggiamento e i contabili IEEE raramente vanno d'accordo .

    
risposta data 09.06.2018 - 23:47
fonte
6

Non hai fornito la separazione stato / strategia o ridotto le responsabilità di Account . Per un chiamante di Account non vi è alcuna separazione tra Account e ActiveAccountState . Qualsiasi chiamata ai metodi di Account chiamerà anche i metodi di ActiveAccountState . Il risultato atteso dipenderà quindi dalla classe Account e ActiveAccountState . I test unitari per Account saranno sensibili alle modifiche in ActiveAccountState .

Se li separi realmente iniettando un'istanza IAccountState in Account , puoi testare Account utilizzando un'implementazione fittizia di IAccountState . Questi test non saranno più sensibili alle modifiche a ActiveAccountState . Ciò complicherà la creazione di istanze Account e potrebbe essere utile una classe factory o un contenitore per l'inserimento delle dipendenze.

Come sapere? Potresti provare a scrivere prima il test del codice. Non è divertente scrivere test complicati, quindi inizierai a ottimizzare il design della tua classe per semplificare i test. Il codice facile da testare è facile da mantenere.

BTW, perché Balance è un membro di Account ? Sicuramente Saldo dovrebbe appartenere allo stato dell'account.

    
risposta data 10.06.2018 - 00:27
fonte
2

ActiveAccountState è chiaramente una dipendenza, ma la domanda è se si dovrebbe iniettare questa dipendenza o crearla all'interno dell'account. In questo caso non vedo alcun beneficio per l'iniezione.

L'account crea un'istanza ActiveAccountState, ma ciò significa che non significa che Account ora "sa come produrre ActiveAccountState". Questo knowlede è presumibilmente incapsulato all'interno del costruttore di ActiveAccountState. L'account utilizza solo l'interfaccia pubblica, proprio come sta usando l'interfaccia pubblica per eseguire le transizioni di stato.

In generale, vuoi esporre il minor numero possibile di elementi interni. Mi sembra che ActiveAccountState sia puramente un meccanismo interno all'account e non debba essere esposto ai clienti dell'account. MaRequire una dipendenza da iniettare significa esporre questa dipendenza al client.

    
risposta data 09.06.2018 - 23:33
fonte
1

Io personalmente applico il Modello di responsabilità singola anche a questo problema:

Una classe che fornisce comportamento aziendale non dovrebbe essere responsabile dell'istanziazione delle sue dipendenze.

Quindi, nel dubbio, preferisci DI.

    
risposta data 10.06.2018 - 00:04
fonte
1

My question would be how to know if something should be a dependency?

Partendo da un solido design di classe OO, le dipendenze tendono a diventare evidenti.

IAccountState sta inventando una dipendenza disaccoppiando in modo inappropriato il comportamento e lo stato di una classe altrimenti coerente. Eppure hai praticamente colpito l'unghia in testa: "Io, bene, espongo i dettagli dell'implementazione che è probabilmente solo interessante per la classe Account e per nessun altro"

Una classe astratta in questo caso può risolvere la frattura di classe mentre insiste su un modello strategico ed elimina così il problema della dipendenza artificiale. E ovunque ci sia un costruttore o un metodo - concreto, astratto o altro - c'è una linea per l'iniezione della dipendenza.

Quindi sono entusiasticamente in disaccordo con Shying away from inheritance perché "hai poche opportunità per DI ... mentre fai la logica ... attraverso la sua catena ereditaria". Dovresti avere progettato opportunità per DI, non un free-for-all che viola gli inquilini dell'orientamento agli oggetti.

Una buona classe espone funzionalità e nasconde lo stato

La chiave sta identificando quella singola responsabilità per ogni classe. Pensare prima alla funzionalità, quindi allo stato necessario per supportarlo. Questo è un percorso più affidabile per una buona applicazione SRP.

dipendenze dei vincoli di progettazione diligente

Non consentire l'esuberanza per l'applicazione SRP di interface s hobble e lo stato di strappo e / o funzionalità prematuramente.

L'ereditarietà e la composizione sono complementari. Lascia che questo si evolva durante l'analisi e il design. In effetti, mi aspetto di vederlo proprio come ogni macchina è composta da parti distinte.

Non avvicinarti a questa soluzione cercando una posizione problematica.

    
risposta data 12.06.2018 - 19:31
fonte
0

Usa sempre DI quando hai separato i dubbi usando la composizione.

Il fatto stesso che stai usando la composizione implica che vorresti cambiare la dipendenza, anche se solo con una simulazione per il test.

Se usi l'ereditarietà, allora hai delle opportunità limitate per DI, mentre inforni la logica nella classe concreta attraverso la sua catena di ereditarietà.

Quindi nel tuo esempio potresti dire account commerciali e correnti con diverse logiche di prelievo gestite da diversi oggetti AccountState iniettati.

Se si utilizza l'ereditarietà si creano classi BusinessAccount e CurrentAccount separate che annullano il metodo di prelievo

    
risposta data 10.06.2018 - 16:50
fonte