L'inserimento delle dipendenze (DI) non è un concetto facile da comprendere e applicarlo ad applicazioni nuove o esistenti crea ancora più confusione. Jess Smith ti mostra come eseguire l'inserimento delle dipendenze senza un contenitore di iniezione nei linguaggi di programmazione C# e Java. In questo articolo ti mostrerò come implementare l'inserimento delle dipendenze (DI) nelle applicazioni .NET e Java. Il concetto di inserimento delle dipendenze attirò per la prima volta l'attenzione degli sviluppatori nel 2000, quando Robert Martin scrisse l'articolo "Design Principles and Patterns" (più tardi conosciuto con l'acronimo
SOLID ). La D in SOLID si riferisce alla Dipendenza di Inversione (DOI), che in seguito divenne nota come iniezione di dipendenza. La definizione originale e più comune: l'inversione delle dipendenze è un'inversione del modo in cui una classe base gestisce le dipendenze. L'articolo originale di Martin utilizzava il seguente codice per illustrare la dipendenza di una classe
Copy
da una classe di livello inferiore
WritePrinter
:
void Copy()
{
int c;
while ((c = ReadKeyboard()) != EOF)
WritePrinter(c);
}
Il primo problema ovvio è che se si modifica l'elenco dei parametri o i tipi di un metodo
WritePrinter
, è necessario implementare gli aggiornamenti ovunque esista una dipendenza da quel metodo. Questo processo aumenta i costi di manutenzione ed è una potenziale fonte di nuovi errori.
Altro problema: la classe Copy non è più una potenziale candidata al riutilizzo. Ad esempio, cosa succede se è necessario inviare i caratteri immessi dalla tastiera a un file anziché a una stampante? Per fare ciò, puoi modificare la classe
Copy
come segue (sintassi del linguaggio C++):
void Copy(outputDevice dev)
{
int c;
while ((c = ReadKeyboard()) != EOF)
if (dev == printer)
WritePrinter(c);
else
WriteDisk(c);
}
Nonostante l’introduzione di una nuova dipendenza
WriteDisk
, la situazione non migliorò (ma anzi peggiorò) perché venne violato un altro principio: “le entità software, cioè classi, moduli, funzioni, ecc., dovrebbero essere aperte per l’estensione, ma chiuse per modifica." Martin spiega che queste nuove istruzioni condizionali if/else riducono la stabilità e la flessibilità del codice. La soluzione è invertire le dipendenze in modo che i metodi di scrittura e lettura dipendano da
Copy
. Invece di "eseguire" le dipendenze, queste vengono passate attraverso il costruttore. Il codice modificato è simile al seguente:
class Reader
{
public:
virtual int Read() = 0;
};
class Writer
{
public:
virtual void Write(char) = 0;
};
void Copy(Reader& r, Writer& w)
{
int c;
while((c=r.Read()) != EOF)
w.Write(c);
}
Ora la classe
Copy
può essere facilmente riutilizzata con diverse implementazioni di metodi di classe
Reader
e
Writer
. La classe
Copy
non ha alcuna informazione sulla struttura interna dei tipi
Reader
e
Writer
, rendendo possibile il riutilizzo con implementazioni diverse. Ma se tutto questo vi sembra una sorta di sciocchezza, forse i seguenti esempi in Java e C# chiariranno la situazione.
Esempio in Java e C#
Per illustrare la facilità dell'inserimento delle dipendenze senza un contenitore di dipendenze, iniziamo con un semplice esempio che può essere personalizzato per l'uso
DI
in pochi passaggi. Diciamo di avere una classe
HtmlUserPresentation
che, quando vengono chiamati i suoi metodi, genera un'interfaccia utente HTML. Ecco un semplice esempio:
HtmlUserPresentation htmlUserPresentation = new HtmlUserPresentation();
String table = htmlUserPresentation.createTable(rowTableVals, "Login Error Status");
Qualsiasi progetto che utilizzi questo codice di classe avrà una dipendenza dalla classe
HtmlUserPresentation
, con conseguenti problemi di usabilità e manutenibilità descritti sopra. Si suggerisce subito un miglioramento: creare un'interfaccia con le firme di tutti i metodi attualmente disponibili nella classe
HtmlUserPresentation
. Ecco un esempio di questa interfaccia:
public interface IHtmlUserPresentation {
String createTable(ArrayList rowVals, String caption);
String createTableRow(String tableCol);
}
Dopo aver creato l'interfaccia, modifichiamo la classe
HtmlUserPresentation
per usarla. Tornando all'istanziazione del type
HtmlUserPresentation
, ora possiamo utilizzare il tipo di interfaccia invece del tipo base:
IHtmlUserPresentation htmlUserPresentation = new HtmlUserPresentation();
String table = htmlUserPresentation.createTable(rowTableVals, "Login Error Status");
La creazione di un'interfaccia ci consente di utilizzare facilmente altre implementazioni di
IHtmlUserPresentation
. Ad esempio, se vogliamo testare questo tipo, possiamo facilmente sostituire il tipo base
HtmlUserPresentation
con un altro tipo chiamato
HtmlUserPresentationTest
. Le modifiche apportate finora rendono il codice più semplice da testare, mantenere e scalare, ma non fanno nulla per il riutilizzo poiché tutte le
HtmlUserPresentation
classi che utilizzano il tipo sono ancora consapevoli della sua esistenza. Per rimuovere questa dipendenza diretta, puoi passare un tipo di interfaccia
IHtmlUserPresentation
al costruttore (o un elenco di parametri del metodo) della classe o del metodo che lo utilizzerà:
public UploadFile(IHtmlUserPresentation htmlUserPresentation)
Il costruttore
UploadFile
ora ha accesso a tutte le funzionalità del tipo
IHtmlUserPresentation
, ma non sa nulla della struttura interna della classe che implementa questa interfaccia. In questo contesto, l'inserimento del tipo avviene quando viene creata un'istanza della classe
UploadFile
. Un tipo di interfaccia
IHtmlUserPresentation
diventa riutilizzabile passando implementazioni diverse a classi o metodi diversi che richiedono funzionalità diverse.
Conclusione e raccomandazioni per consolidare il materiale
Hai appreso l'inserimento delle dipendenze e che si dice che le classi dipendano direttamente l'una dall'altra quando una di esse ne istanzia un'altra per ottenere l'accesso alle funzionalità del tipo di destinazione. Per disaccoppiare la dipendenza diretta tra i due tipi, dovresti creare un'interfaccia. Un'interfaccia offre a un tipo la possibilità di includere diverse implementazioni, a seconda del contesto della funzionalità richiesta. Passando un tipo di interfaccia a un costruttore o a un metodo di classe, la classe/metodo che necessita della funzionalità non conosce alcun dettaglio sul tipo che implementa l'interfaccia. Per questo motivo, un tipo di interfaccia può essere riutilizzato in classi diverse che richiedono un comportamento simile, ma non identico.
- Per sperimentare l'inserimento delle dipendenze, osserva il tuo codice da una o più applicazioni e prova a convertire un tipo di base molto utilizzato in un'interfaccia.
- Modificare le classi che istanziano direttamente questo tipo base per utilizzare questo nuovo tipo di interfaccia e passarlo attraverso il costruttore o l'elenco di parametri del metodo della classe che lo utilizzerà.
- Crea un'implementazione di test per testare questo tipo di interfaccia. Una volta effettuato il refactoring, il codice
DI
diventerà più semplice da implementare e noterai quanto più flessibile diventa la tua applicazione in termini di riutilizzo e manutenibilità.
GO TO FULL VERSION