Nella programmazione a oggetti, si sente spesso parlare di Dependency Inversion e Inversion of Control (Principio di inversione delle Dipendenze, Inversione del Controllo in italiano). Forse, i termini vi sono noti perché avete utilizzato Spring, ma sapete distinguerne le caratteristiche o comprenderne l’importanza?
In questo articolo cercherò di chiarire e introdurre i concetti che riguardano il Dependency Inversion, Inversion of Control e Dependency Injection.
Dependency Inversion, la base
Il Dependency Inversion è uno dei cinque principi SOLID dello sviluppo del software orientato agli oggetti. È tutto iniziato con un semplice concetto introdotto da Robert C. Martin (Uncle Bob) in un suo articolo del 1996:
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. Questo principio è spesso frainteso o applicato male, perché se ne trascurano le ragioni che vi sono implicate.
Che problema risolve il Dipendency Inversion
Il principio dell’inversione della dipendenza risolve il problema per cui i moduli software di alto livello sono dipendenti o uniti alle interfacce dei moduli di più basso livello e ai loro dettagli.
Facciamo un esempio del mondo reale, una situazione che penso ognuno di noi conosce e che potrebbe essere risolta se venisse applicato il principio di inversione della dipendenza.
Prendiamo casa nostra e immaginiamo che sia il componente software di alto livello. Guardiamoci attorno e elenchiamo tutti i dispositivi che hanno una batteria e la possibilità di essere ricaricati (i moduli software di più basso livello):
- cellulari, tablet
- fotocamere
- controller wireless
- cuffie wireless
- …
Cosa hanno in comune tutti questi “moduli”? Possono essere ricaricati, ma hanno tutti un’interfaccia di ricarica diversa. Qualcuno utilizza micro usb, le mie cuffie wireless mini-usb, il tablet un connettore proprietario e così via (è vero, oggi quasi tutti microusb o usb 3.0).
Come conseguenza, non puoi avere un unico aggeggio che ricarica tutti i tuoi dispositivi. Devi possedere caricatori differenti per ogni dispositivo. Il modulo “ricarica dispositivi mobili” della tua casa è dipendente ai dispositivi. Se cambi dispositivo, hai bisogno di un nuovo caricatore.
La dipendenza è orientata in maniera errata: i moduli di basso livello stanno definendo l’interfaccia che il modulo di alto livello (la mia casa) deve usare per caricarli. Dovrebbe essere il modulo “ricarica dispositivi mobili” della mia casa a definire l’interfaccia che i dispositivi dovrebbero usare. La dipendenza dovrebbe essere invertita: semplificherebbe la vita e ridurrebbe le spese.
In effetti, è proprio quello che è avvenuto nelle nostre case: è stato approvata una direttiva europea per un formato standard a cui i vari produttore di dispositivi dovrebbero adeguarsi. C’è stata una inversione della dipendenza.
Torniamo al codice
Immaginiamo che stiamo sviluppando un modulo di alto livello per il parsing di file LOG. Vorremo poter gestire differenti formati di log da diverse sorgenti e riportare delle informazioni comuni su un unico database.
Un approccio a questo genere di problema potrebbe essere quello di gestire diversi formati di file di log e le diverse sorgenti nel nostro modulo. In questo modo, il nostro modulo deve poter gestire ogni singolo modo in cui questi file vengono forniti.
Il problema è che i file di log stanno definendo l’interfaccia che il nostro codice di alto livello dovrebbe usare. In parole povere, stanno controllando il nostro codice, definendo il comportamento a cui il nostro codice deve conformarsi.
Possiamo invertire questo controllo e invertire le dipendenze specificando una interfaccia a cui i file di log che intendiamo processare devono conformarsi.
Ad esempio, possiamo definire una classe chiamata LogLine
che rappresenta l’input del nostro modulo. Chi vuole utilizzare il nostro parser deve convertire i suoi file nel nostro formato.
In alternativa, si potrebbe definire una interfaccia ILogFile
che potrebbe essere implementata da specifiche classi che definiscono la logica per effettuare il parsing del file. Il nostro programma dipenderebbe solo da ILogFile
.
Il punto è che il nostro codice dovrebbe controllare l’interfaccia a cui i moduli di basso livello devono aderire, invece di dover soddisfare i capricci di ogni modulo che vuole interfacciarsi con noi.
Quando (non) usare l’inversione di dipendenza
Nel nostro esempio noi stiamo sviluppando qualcosa che deve interfacciarsi a diversi formati di file. Se noi stessimo scrivendo un programma che deve analizzare solo uno specifico formato, non c’è nessuna convenienza nell’invertire la dipendenza. Se il codice viene scritto in maniera pulita e corretta, potrebbe essere più semplice effettuare a posteriori un refactoring per invertire le dipendenze e gestire diversi formati di file.
Solo perché è applicabile il principio di inversione della dipendenza, non significa che dobbiamo applicarlo.
Dependency Inversion e Inversion of Control
Prima di tutto, definiamo cosa si intende per controllo che può essere invertito:
- il controllo dell’interfaccia (come interagiscono questi due sistemi, moduli, classi tra di loro e come scambiano dati?)
- il controllo del flusso. (cosa controlla il flusso del programma?)
- il controllo del dependancy creation and binding (chi crea e inizializza gli oggetti e le loro dipendenze?)
Ognuno di questi controlli introduce un controllo della dipendenza e coinvolge diversi tipi di dipendenze che possono essere invertite.
Quando qualcuno parla di Inversion of Control, chiediti “Che tipo di controllo viene invertito qui?”
L’inversione della dipendenza è un principio progettuale. L’inversione del controllo specifica un pattern (una soluzione generale ad un problema ricorrente) da applicare per implementare l’inversione della dipendenza.
Il punto 3, invertire la dipendenza della creazione, è dove l’IoC regna sovrana.
E il Dependency Injection?
Il Dependency Injection è un design pattern che attua l’inversione del controllo proprio per la creazione e per la risoluzione delle dipendenze di un oggetto. Viene dichiarate le dipendenze che un componente necessità, quando il componente viene istanziato (creato), un iniettore si prende carico di risolvere le dipendenze attuando una vera e propria inversione del controllo.
Conclusioni
Abbiamo compreso che l’Inversione della dipendenza (Dependency Inversion) è il principio cardine da cui sono derivate molte altre pratiche. L’obiettivo è rendere le componenti software il più indipendenti possibili.
Un modo per applicare questo principio è definito nel pattern dell’Inversione del controllo (Inversion of control) per cui un componente di alto livello riceve il controllo da un componente di più basso livello.
Una delle tecniche con le quali si può attuare l’inversione del controllo è il Dependency Injection. Esso prende il controllo su tutti gli aspetti di creazione degli oggetti e delle loro dipendenze.
Ogni volta che decidiamo di applicare un design pattern, dobbiamo esaminare il principio su cui si fonda e il tipo di problema che intende aiutarci a risolvere.
Ho iniziato a scrivere questo articolo dopo che un amico mi ha detto che in un colloquio gli hanno chiesto quali sono i benefici di Spring. Ha citato l’IoC, ma non ha saputo spiegare cosa fosse e quali benefici realmente arreca l’inversione del controllo.
Spero di essere riuscito a rispondere a questo mio amico e di fornire delle basi per comprendere meglio questo solido principio della programmazione ad oggetti.