Il principio di inversione delle dipendenze (DIP) Γ¨ un principio di progettazione del software che mira a ridurre l'accoppiamento tra le classi e a promuovere una struttura di codice piΓΉ modulare. Lβidea di base Γ¨ quella di invertire le dipendenze tra classi ad alto e basso livello, in modo che entrambe dipendano da astrazioni invece che da implementazioni concrete. Per astrazioni si intende interfaccie (anche le classi astratte sono astrazioni ma non sono mockabili facilmente rendendo il codice meno testabile).
Il DIP puΓ² essere sintetizzato in due asserzioni principali:
- Le classi ad alto livello non dovrebbero dipendere dalle classi a basso livello. Entrambe dovrebbero dipendere dalle astrazioni.
- Le astrazioni non dovrebbero dipendere dai dettagli. I dettagli dovrebbero dipendere dalle astrazioni.
PerchΓ© Γ¨ importante
Il DIP Γ¨ importante perchΓ© aiuta a ridurre lβaccoppiamento tra i moduli, migliorando la modularitΓ , la manutenibilitΓ e la testabilitΓ del software. Un alto grado di accoppiamento tra i moduli puΓ² portare a problemi come:
- DifficoltΓ nella manutenzione: Quando i moduli sono strettamente accoppiati, modifiche in un modulo potrebbero richiedere cambiamenti in altri moduli, rendendo la manutenzione del software complessa e dispendiosa in termini di tempo.
- Ridotta testabilitΓ : I moduli strettamente accoppiati possono rendere difficile isolare e testare singole parti del software, poichΓ© i test potrebbero richiedere la configurazione e lβesecuzione di numerosi moduli dipendenti.
- RigiditΓ nellβarchitettura: Unβelevata dipendenza tra i moduli puΓ² rendere difficile la sostituzione o lβestensione di parti del software senza influenzare altri moduli, limitando la flessibilitΓ dellβarchitettura.
Dipendendo dalle astrazioni anzichΓ© dalle implementazioni concrete, si promuove lβuso di dependency injection e di inversion of control (IoC) containers. CiΓ² rende la tua codebase piΓΉ adattabile alle modifiche, poichΓ© puoi facilmente sostituire le implementazioni senza modificare le classi dipendenti. DIP facilita anche migliori unit test, poichΓ© le dipendenze possono essere facilmente mockate.
Come applicarlo
Data una classe per applicare il principio basta rilevare quali sono le classi concrete che vengono utilizzate e sostituirle da interfacce che tipicamente vengono fornite a costruttore o tramite factory o automaticamente con Dependency Injection Framework. Tutto qui. La classe risulterΓ cosΓ¬ facilmente testabile in quanto il test creerΓ un mock per lβinterfaccia e la passerΓ a costruttore della classe, modificando cosΓ¬ il comportamento della stessa. Se invece voglio cambiare il comportamento runtime posso, invece di passare lβoggetto a costruttore, passarlo tramite property (a patto che il suo tipo sia sempre unβastrazione) seguendo il pattern strategy.
Limiti
- ComplessitΓ aggiuntiva: Lβapplicazione del DIP puΓ² introdurre una complessitΓ aggiuntiva nel sistema, poichΓ© richiede la creazione di astrazioni (per separare le dipendenze tra classi di alto e basso livello.
- Sovraprogettazione: Il DIP puΓ² portare alla sovraprogettazione se gli sviluppatori cercano di applicarlo in ogni situazione, anche quando non Γ¨ strettamente necessario.
Esempi
Esempio 1
Ecco un esempio in C# per illustrare il concetto:
- Creiamo unβinterfaccia per definire unβastrazione comune:
// Interfaccia che rappresenta un generico servizio di notifica
public interface INotificationService
{
// Metodo per inviare una notifica
void SendNotification(string message);
}
- Implementiamo lβinterfaccia con classi concrete:
// Implementazione concreta del servizio di notifica tramite email
public class EmailNotificationService : INotificationService
{
public void SendNotification(string message)
{
// Logica per inviare l'email con il messaggio
}
}
// Implementazione concreta del servizio di notifica tramite SMS
public class SmsNotificationService : INotificationService
{
public void SendNotification(string message)
{
// Logica per inviare l'SMS con il messaggio
}
}
- Creiamo una classe ad alto livello che utilizzi il servizio di notifica:
// Classe ad alto livello che rappresenta un'applicazione
public class Application
{
// Dipendiamo dall'interfaccia, non dalle implementazioni concrete
private readonly INotificationService _notificationService;
// Usiamo l'iniezione di dipendenza per passare un'implementazione concreta dell'interfaccia
public Application(INotificationService notificationService)
{
_notificationService = notificationService;
}
// Metodo che utilizza il servizio di notifica
public void NotifyUser(string message)
{
_notificationService.SendNotification(message);
}
}
- Infine, nel nostro programma principale, creiamo unβistanza della classe Application e passiamo unβimplementazione concreta del servizio di notifica:
public static void Main()
{
// Creiamo un'istanza del servizio di notifica via email
INotificationService emailNotificationService = new EmailNotificationService();
// Creiamo un'istanza dell'applicazione e passiamo il servizio di notifica desiderato
Application app = new Application(emailNotificationService);
// Usiamo il metodo NotifyUser per inviare una notifica
app.NotifyUser("Ciao, questo Γ¨ un messaggio di prova!");
}
In questo esempio, abbiamo seguito il Dependency Inversion Principle creando unβinterfaccia comune INotificationService
e facendo dipendere la classe ad alto livello Application
dallβinterfaccia, invece che dalle implementazioni concrete. Inoltre, abbiamo utilizzato lβiniezione di dipendenza per passare unβimplementazione concreta dellβinterfaccia alla classe Application
.
Esempio 2
// Violates DIP
public class User
{
private SqlContext _context;
public User(SqlContext context)
{
_context = context;
}
public void Add(string userName)
{
_context.AddUser(userName);
}
}
// Adheres to DIP
public interface IContext
{
void AddUser(string userName);
}
public class SqlContext : IContext
{
public void AddUser(string userName)
{
// Add user to SQL database
}
}
public class User
{
private IContext _context;
public User(IContext context)
{
_context = context;
}
public void Add(string userName)
{
_context.AddUser(userName);
}
}