Quante iniezioni sono accettabili in una classe quando si usa l'iniezione di dipendenza


9

Sto usando Unity in C # per l'iniezione di dipendenza, ma la domanda dovrebbe essere applicabile per qualsiasi linguaggio e framework che sta usando l'iniezione di dipendenza.

Sto cercando di seguire il principio SOLID e quindi ho avuto molte astrazioni. Ma ora mi chiedo se ci sia una buona pratica per quante iniezioni una classe dovrebbe iniettare?

Ad esempio, ho un repository con 9 iniezioni. Sarà difficile da leggere per un altro sviluppatore?

Le iniezioni hanno le seguenti responsabilità:

  • IDbContextFactory: creazione del contesto per il database.
  • IMapper: mappatura da entità a modelli di dominio.
  • IClock - Abstracts DateTime.Oow per aiutare con i test unitari.
  • IPerformanceFactory: misura il tempo di esecuzione per metodi specifici.
  • ILog - Log4net per la registrazione.
  • ICollectionWrapperFactory - Crea raccolte (che estende IEnumerable).
  • IQueryFilterFactory: genera query basate sull'input che eseguirà query su db.
  • IIdentityHelper: recupera l'utente che ha effettuato l'accesso.
  • IFaultFactory - Crea diverse FaultExceptions (utilizzo WCF).

Non sono deluso dal modo in cui ho delegato le responsabilità, ma sto iniziando a preoccuparmi per la leggibilità.

Quindi, le mie domande:

Esiste un limite per quante iniezioni dovrebbe avere una classe? E se sì, come evitarlo?

Molte iniezioni limitano la leggibilità o la migliorano effettivamente?


2
Misurare la qualità in base ai numeri è generalmente negativo come essere pagato da righe di codice che scrivi al mese. Tuttavia, hai incluso l'esempio reale delle dipendenze, che è un'idea eccellente. Se fossi in te, riformulerei la tua domanda per rimuovere il concetto di conteggio e di limite rigoroso e focalizzarmi invece sulla qualità in sé. Mentre la riformuli, fai attenzione a non rendere la tua domanda troppo specifica.
Arseni Mourzenko,

3
Quanti argomenti del costruttore sono accettabili? IoC non lo cambia .
Telastyn,

Perché le persone vogliono sempre limiti assoluti?
Marjan Venema,

1
@Telastyn: non necessariamente. Ciò che cambia IoC è che invece di fare affidamento su classi statiche / singoli / variabili globali, tutte le dipendenze sono più centralizzate e più "visibili".
Arseni Mourzenko,

@MarjanVenema: perché semplifica la vita. Se conosci esattamente il massimo LOC per metodo o il numero massimo di metodi in una classe o il numero massimo di variabili in un metodo, e questa è l'unica cosa che conta, diventa facile qualificare il codice come buono o cattivo, come oltre a "correggere" il codice errato. Questo è un peccato che la vita reale sia molto più complessa di così e molte metriche sono per lo più irrilevanti.
Arseni Mourzenko,

Risposte:


11

Troppe dipendenze possono indicare che la classe stessa sta facendo troppo. Per determinare se non sta facendo troppo:

  • Guarda la classe stessa. Avrebbe senso dividerlo in due, tre, quattro? Ha senso nel suo insieme?

  • Guarda i tipi di dipendenze. Quali sono specifici del dominio e quali sono "globali"? Ad esempio, non prenderei ILogin considerazione lo stesso livello di IQueryFilterFactory: il primo sarebbe comunque disponibile nella maggior parte delle classi aziendali se utilizzano la registrazione. D'altra parte, se trovi molte dipendenze specifiche del dominio, ciò potrebbe indicare che la classe sta facendo troppo.

  • Guarda le dipendenze che possono essere sostituite da valori.

    IClock - Abstracts DateTime.Oow per aiutare con i test unitari.

    Questo potrebbe essere facilmente sostituito DateTime.Nowpassando direttamente ai metodi che richiedono di conoscere l'ora corrente.

Osservando le dipendenze effettive, non vedo nulla che possa essere indicativo di cose brutte che accadono:

  • IDbContextFactory: creazione del contesto per il database.

    OK, probabilmente siamo all'interno di un livello aziendale in cui le classi interagiscono con il livello di accesso ai dati. Sembra a posto.

  • IMapper: mappatura da entità a modelli di dominio.

    È difficile dire qualcosa senza il quadro generale. È possibile che l'architettura sia errata e che la mappatura debba essere eseguita direttamente dal livello di accesso ai dati o che l'architettura sia perfettamente a posto. In tutti i casi, ha senso avere questa dipendenza qui.

    Un'altra scelta sarebbe quella di dividere la classe in due: uno che si occupa della mappatura, l'altro che si occupa della logica aziendale reale. Ciò creerebbe uno strato di fatto che separerà ulteriormente il BL dal DAL. Se i mapping sono complessi, potrebbe essere una buona idea. Nella maggior parte dei casi, tuttavia, aggiungerebbe solo una complessità inutile.

  • IClock - Abstracts DateTime.Oow per aiutare con i test unitari.

    Probabilmente non è molto utile avere un'interfaccia (e una classe) separate solo per ottenere l'ora corrente. Vorrei semplicemente passare DateTime.Nowai metodi che richiedono l'ora corrente.

    Una classe separata può avere senso se ci sono altre informazioni, come i fusi orari o gli intervalli di date, ecc.

  • IPerformanceFactory: misura il tempo di esecuzione per metodi specifici.

    Vedi il prossimo punto

  • ILog - Log4net per la registrazione.

    Tale funzionalità trascendente dovrebbe appartenere al framework e le librerie effettive dovrebbero essere intercambiabili e configurabili in fase di esecuzione (ad esempio tramite app.config in .NET).

    Sfortunatamente, questo non è (ancora) il caso, che consente di scegliere una libreria e attenersi ad essa, o creare un livello di astrazione per essere in grado di scambiare le librerie in seguito, se necessario. Se la tua intenzione è specificamente quella di essere indipendente dalla scelta della biblioteca, provaci. Se sei abbastanza sicuro che continuerai a utilizzare la libreria per anni, non aggiungere un'astrazione.

    Se la libreria è troppo complessa da usare, ha senso un motivo di facciata.

  • ICollectionWrapperFactory - Crea raccolte (che estende IEnumerable).

    Suppongo che questo crei strutture di dati molto specifiche utilizzate dalla logica del dominio. Sembra una classe di utilità. Utilizzare invece una classe per struttura dati con costruttori pertinenti. Se la logica di inizializzazione è leggermente complicata da inserire in un costruttore, utilizzare metodi di fabbrica statici. Se la logica è ancora più complessa, utilizzare il modello factory o builder.

  • IQueryFilterFactory: genera query basate sull'input che eseguirà query su db.

    Perché non è quello nel livello di accesso ai dati? Perché c'è un Filternel nome?

  • IIdentityHelper: recupera l'utente che ha effettuato l'accesso.

    Non sono sicuro del motivo per cui esiste un Helpersuffisso. In tutti i casi, anche altri suffissi non saranno particolarmente espliciti ( IIdentityManager?)

    Comunque, ha perfettamente senso avere questa dipendenza qui.

  • IFaultFactory - Crea diverse FaultExceptions (utilizzo WCF).

    È la logica così complessa da richiedere un modello di fabbrica? Perché viene utilizzata l'iniezione di dipendenza? Scambieresti la creazione di eccezioni tra codice di produzione e test? Perché?

    Vorrei provare a trasformarlo in semplice throw new FaultException(...). Se è necessario aggiungere alcune informazioni globali a tutte le eccezioni prima di propagarle al client, WCF probabilmente ha un meccanismo in cui si rileva un'eccezione non gestita e può cambiarla e riproporla al client.

Esiste un limite per quante iniezioni dovrebbe avere una classe? E se sì, come evitarlo?

Misurare la qualità in base ai numeri è generalmente negativo come essere pagato da righe di codice che scrivi al mese. Potresti avere un elevato numero di dipendenze in una classe ben progettata, dato che puoi avere una classe scadente usando poche dipendenze.

Molte iniezioni limitano la leggibilità o la migliorano effettivamente?

Molte dipendenze rendono la logica più difficile da seguire. Se la logica è difficile da seguire, la classe probabilmente sta facendo troppo e dovrebbe essere divisa.


Grazie per i tuoi commenti e il tuo tempo. Principalmente su FaultFactory che verrà invece spostato nella logica WCF. Terrò IClock dal momento che salva la vita quando TDD-in un'applicazione. Diverse volte si desidera assicurarsi che un determinato valore sia stato impostato in un determinato momento. Quindi DateTime.Now non sarà sempre sufficiente poiché non è beffardo.
smoksnes,

7

Questo è un classico esempio di DI che ti dice che probabilmente la tua classe sta diventando troppo grande per essere una singola classe. Questo è spesso interpretato come "wow, DI rende i miei costruttori più grandi di Giove, questa tecnica è orribile", ma ciò che effettivamente ti dice è che "la tua classe ha un carico di dipendenze". Sapendo questo, possiamo farlo entrambi

  • Spazzare il problema sotto il tappeto iniziando invece a rinnovare le dipendenze
  • Riconsidera il nostro design. Forse alcune dipendenze si incontrano sempre e dovrebbero essere nascoste dietro qualche altra astrazione. Forse la tua classe dovrebbe essere divisa in 2. Forse dovrebbe essere composta da diverse classi che richiedono ciascuna un piccolo sottoinsieme delle dipendenze.

Esistono innumerevoli modi per gestire le dipendenze ed è impossibile dire cosa funzioni meglio nel tuo caso senza conoscere il tuo codice e la tua applicazione.

Per rispondere alle tue ultime domande:

  • C'è un limite massimo a quante dipendenze dovrebbe avere una classe?

Sì, il limite superiore è "troppi". Quanti sono "troppi"? "Troppi" è quando la coesione della classe diventa "troppo bassa". Tutto dipende. Normalmente se la tua reazione a una classe è "wow, questa cosa ha molte dipendenze", è troppo.

  • L'iniezione di dipendenze migliora o danneggia la leggibilità?

Penso che questa domanda sia fuorviante. La risposta può essere sì o no. Ma non è la parte più interessante. Il punto di iniettare dipendenze è renderle VISIBILI. Si tratta di creare un'API che non mente. Si tratta di prevenire lo stato globale. Si tratta di rendere testabile il codice. Si tratta di ridurre l'accoppiamento.

Le classi ben progettate con metodi ragionevolmente progettati e denominati sono più leggibili di quelle mal progettate. DI non migliora davvero né danneggia la leggibilità di per sé, fa solo risaltare le tue scelte di design e se sono cattive ti farà male agli occhi. Questo non significa che DI abbia reso il tuo codice meno leggibile, ti ha solo mostrato che il tuo codice era già un disastro, l'avevi appena nascosto.


1
Buon riscontro Grazie. Sì, il limite superiore è "troppi". - Fantastico e così vero.
smoksnes,

3

Secondo Steve McConnel, autore dell'onnipresente "Codice completo", la regola empirica è che più di 7 è un odore di codice che fa male alla manutenibilità. Personalmente penso che il numero sia più basso nella maggior parte dei casi, ma fare correttamente DI comporterà un'iniezione di molte dipendenze quando sei molto vicino al Root di composizione. Questo è normale e previsto. È uno dei motivi per cui i contenitori IoC sono utili e meritano la complessità che aggiungono a un progetto.

Quindi, se sei molto vicino al punto di ingresso del tuo programma, questo è normale e accettabile. Se sei più approfondito nella logica del programma, è probabilmente un odore che dovrebbe essere affrontato.

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.