DIP, Dependency Inversion Principle

_Logo Dal punto di vista formale è apparentemente il più complesso dei principi SOLID e nello stesso tempo uno dei più utili. Si tratta di applicare una tecnica di disaccoppiamento dei moduli per gestire al meglio le dipendenze, che limitano la testabilità e la manutenibilità del codice.

Per accoppiamento o dipendenza si intende il grado con cui ciascun componente di un programma fa affidamento su un altro componente, dipendendo, appunto, da esso.

La prima forma di dipendenze e naturalmente tra i propri moduli: se un classe utilizza direttamente altri tipi concreti, allora per poter utilizzare quella classe dovrò sempre istanziare i tipi concreti limitando la possibilità di eseguire test e implementare estensioni.

Anche utilizzare un Framework o una libreria di terze parti genera una dipendenza perché a quei componenti si possono ripercuotere su tutti i punti dell'applicazione che ne fanno uso. Tipiche dipendenze riguardano l'accesso a risorse esterne come Database, File System, Mail Server, Web Services. La considerazione più semplice è che se in un certo momento non sono disponibili non possiamo testare i metodi che ne fanno uso.

Ci sono inoltre le risorse di sistema che sono spesso sottovalutate da questo punto di vista: se il comportamento dell’applicazione dipende dalla data e dall’ora corrente, allora si ha una dipendenza. Se utilizzo la classe Random, allora ho un’altra dipendenza. La stessa configurazione dell’applicazione crea una dipendenza: la mia applicazione si comporterà in un modo o in un altro in base alla configurazione in uso.

Una dipendenza è un qualcosa utilizzato dall’applicazione che influenza il comportamento dei moduli dell’applicazione stessa e che rende difficile la creazione di unit testing. Se la risorsa viene modificata è possibile che una parte dell’applicazione non funzioni più e se voglio invece creare una unit test per quella stessa parte, posso trovarmi in difficolta ad utilizzare una risorsa esterna o di sistema.

Si pensi all’utilizzo dell’ora corrente: utilizzarla in un metodo comporta che non sia possibile valutare tutte le casistiche legale a quel metodo. Quello che generalmente è possibile fare è passare un servizio che ritorna l’ora corrente, in modo che nelle unit test possa essere utilizzato un orario fisso in base a ciò che si vuole verificare.

Un modo rapido per individuare le dipendenze in un’applicazione è quella di cercare la creazione diretta degli oggetti, quindi la keyword new e l’uso di metodi statici.

I metodi statici sono considerati smell code: rendono difficile il test perché non possono essere sostituiti facilmente e spesso includono dipendenze nascoste, non espresse nei parametri in input. Ho descritto tali problematiche nell’articolo Considerazioni sui metodi statici.

Dal punto di vista formale la definizione di questo principio è la [seguente][ObjectMentor]:

I moduli di alto livello non devono dipendere da quelli di basso livello. Entrambi devono dipendere da astrazioni. Le astrazioni non devono dipendere dai dettagli. Sono i dettagli che dipendono dalle astrazioni. La formulazione appare oscura ma in realtà quello che viene espresso è sostanzialmente gestire le dipendenze tra i vari elementi dell’applicazione tramite astrazione, quindi classi astratte o interfacce se parliamo di C#.

Se un modulo utilizza un’astrazione allora si potrà modificare o cambiare il modulo che implementa tale astrazione senza richiedere modifiche al modulo dipendente. L’implementazione è infatti il dettaglio della definizione ufficiale ed è chiaro che se voglio mantenere alto il livello di estendibilità e testabilità del mio applicativo utilizzerò le astrazioni anche nei dettagli. L’obiettivo è dipendere dalle astrazioni, molto più flessibili rispetto a tipi concreti.

Il vantaggio di astrarre la dipendenza è quello di poter scegliere l’implementazione più corretta in base al contesto. Quindi durante il test di un metodo potrò creare un’implementazione fake per le dipendenze che tale metodo richiede. Lo scopo di un Automated Code Test è infatti quello di testare la logica del metodo, non il metodo all’interno di un certo contesto.

C’è un interessante aspetto da considerare quando si identifica come smell code la creazione diretta di un oggetto: il fatto che probabilmente la classe che istanzia un’altra classe sta violando il SRP. Il compito di creare gli oggetti non deve essere di tutte le classi ma deve essere centralizzato il più possibile.

Il decidere quali oggetti creare fa parte della business logic e non certo dell’interfaccia utente, ma tradizionalmente si hanno invece applicazioni in cui un layer di alto livello, come quello dedicato alla UI, crea direttamente un oggetto di un layer di un livello più basso. Quindi avremo nel code behind di una maschera di inserimento ordini la creazione degli oggetti business logic per gestire l’ordine e queste classi a loro volta istanzieranno direttamente le classi per accedere al database o al server di posta. Si crea in pratica una catena di dipendenze in cui la modifica di un anello si ripercuote ovunque.

Un modo molto comune e semplice per soddisfare il Dependency Inversion Principle è quello di utilizzare il costruttore come punto di entrata delle dipendenze: è un modo esplicito che permettere di chiarire immediatamente cosa la classe richiedere per il suo scopo. Naturalmente le classi non devono avere dipendenze nascoste/implicite. Se ad esempio il costruttore di una classe che si occupa di creare un ordine ha un costruttore senza parametri ma utilizza nel codice chiamate dirette al database, allora ho una dipendenza nascosta che dovrebbe essere evidenziata passando al costruttore un’astrazione del repository utilizzato. Già il fatto che vi siano chiamate dirette al database viola lo SRP e non considera il Repository Pattern.

Questa tecnica è detta constructor injection ed una delle tre modalità del dependency injection. Le altre due tecniche, meno comuni, sono property injection e parameter injection. In tutti i casi una dipendenza viene iniettata in una classe in base alle necessità da un oggetto esterno che gestire il ciclo di vita delle varie dipendenze.

Anche se spesso il Dependency Inversion Principle è associato al concetto di Dependency Injection e Inversion of Control e quindi di IoC containers, questi ultimi sono uno delle possibili strumenti per rispettare il principio.

Spesso per rispettare tale principio ci si affida allo Stategy Pattern: infatti esso prevede la definizione di un astrazione per definire famiglie di comportamenti intercambiabili. Anche in questo caso, come per gli IoC Container, il pattern è una possibile soluzione. Anzi, essendo un pattern, una traccia di soluzione.

Rispettare il Dependency Inversion Principle significa aumentare la testabilità dell’applicazione e la sua manutenibilità: una modifica ad un oggetto non ha conseguenze inaspettate. L’applicare tale principio permette inoltre di rispettarne un altro: è possibile estendere un’applicazione senza modificare l’esistente, come previso dal OCP.

Altro vantaggio del DIP riguarda le responsabilità delle classi e il principio di singola responsabilità: il processo di identificazione e astrazione delle dipendenze comporta l’individuazione dell’effettivo ruolo di una classe.

Tag: DIP, SOLID