Nella programmazione di tutti i giorni siamo abituati ad operare secondo un pattern ormai collaudato: i componenti 'di servizio', come il gestore di connessione al database, il template engine o il generatore di documenti XML, ordinati in librerie, vengono inclusi dai componenti che integrano la logica di business e da loro utilizzati.
Il problema legato a questa operatività è insito nel profondo legame di dipendenza che si crea fra la
classe che incorpora la logica di business e tutte le librerie di servizio, rendendo di fatto difficile il
riutilizzo della stessa in altri progetti.
Il Dipendency Inversion Principle (DIP), formulato da Robert C. Martin, indica in modo chiaro una tecnica di implementazione pensata per evitare i problemi di poco fa. il tutto si basa
L'Inversione della dipendenza si basa su due assunzioni, un po' criptiche:
- “ I moduli di alto livello non dovrebbero dipendere da moduli di basso livello. Entrambi dovrebbero dipendere da astrazioni. ”
- “ Le astrazioni non dovrebbero dipendere dai dettagli. I dettagli dovrebbero dipendere dalle astrazioni ”
Cerchiamo di gettar luce su quanto enunciato con qualche esempio.
Connessione al database
Per questo articolo utilizziamo un esempio classico e diretto, il driver di connessione al database. Supponiamo di avere una struttura come la seguente:
Una classe Utente
che richiede una classe MySqlDriver
per poter compiere operazioni come:
richiedi MySqlDriver
classe Utente
void salva()
MySqlDriver.scriviSuDatabase('tab_utente',...)
Dov'è il problema? La classe Utente
sarà sempre dipendente dalla classe MySqlDriver
: se avessimo l'opportunità di sviluppare un progetto simile, nel quale però i dati vengono memorizzati su di un'altro database (Oracle, ad esempio), non potremmo beneficiare del codice che abbiamo già scritto ma dovremmo re-implementare da zero la classe. Inoltre anche in questo progetto la classe Utente
è troppo rigida nei confronti di MySqlDriver
.
Applichiamo il punto A enunciato dal principio: I moduli di alto livello non devono dipendere da moduli di basso livello, entrambi devono dipendere da astrazioni.
Dobbiamo spostare la relazione tra Utente
e MySqlDriver
su due classi più generiche, ad esempio:
In questo modo abbiamo ottenuto un duplice beneficio, in primo luogo abbiamo slegato completamente Utente
dalla libreria di servizio, in seconda istanza è ora molto più semplice beneficiare delle funzioni di salvataggio su database dalle classi di alto livello attraverso la classe Modello
:
richiedi Modello
classe Utente che deriva da Modello
void salva
salvaSuDatabase('tab_utente',...)
richiedi DatabaseDriver
classe Modello
costruttore Modello(DatabaseDriver d)
driver = d
void salvaSuDatabase(string tabella, ...)
driver.salvaSuDatabase(tabella, ...)
richiedi DatabaseDriver
classe MySqlDriver che deriva da DatabaseDriver
void salvaSuDatabase(tabella, comando)
// implementazione della funzione di salvataggio
classe DatabaseDriver
void salvaSuDatabase(tabella, comando)
// metodo astratto
L'intera struttura è incredibilmente più flessibile, possiamo infatti creare altre classi che ereditino da Modello
e beneficino quindi automaticamente dell'interazione con il database di riferimento.
Lo stesso discorso vale anche nel senso opposto, nuovi Driver
possono essere tranquillamente inseriti nel progetto senza causare nessuna modifica alle classi utilizzatrici.
A questo si aggiunge la possibilità di riutilizzare sia i componenti che contengono la logica di business sia le librerie di servizio per futuri progetti a patto di mantenere la relazione tra Modello e DatabaseDriver; vincolo accettabile in quanto queste due classi sono così generiche da non causare, almeno in linea teorica, particolari problematiche.
Prima di proseguire notiamo anche che in questo modo abbiamo anche ottenuto l'aderenza al
secondo punto dell'enunciato del principio: l'astrazione non deve dipendere dai dettagli, i dettagli
devono dipendere dall'astrazione.
Ma cosa possiamo fare se MySqlDriver
è una libreria di terze parti sulla quale non possiamo insistere con questo tipo di refactory?
Semplice: utilizziamo un Adapter che sappia tradurre i metodi esposti dalla classe generica (DatabaseDriver) nelle funzioni esposte dalla libreria: