Dovresti usare le tecniche per risolvere i problemi che sono in grado di risolvere quando hai questi problemi. L'inversione di dipendenza e l'iniezione non sono diversi.
L'inversione o l'iniezione di dipendenza è una tecnica che consente al codice di decidere quale implementazione di un metodo viene richiamata in fase di esecuzione. Questo massimizza i benefici del legame tardivo. La tecnica è necessaria quando il linguaggio non supporta la sostituzione del tempo di esecuzione delle funzioni non di istanza. Ad esempio, Java non ha un meccanismo per sostituire le chiamate a un metodo statico con chiamate a un'implementazione diversa; contrasto con Python, dove tutto ciò che è necessario per sostituire la chiamata di funzione è quello di associare il nome a una funzione diversa (riassegnare la variabile che contiene la funzione).
Perché dovremmo voler variare l'implementazione della funzione? Ci sono due ragioni principali:
- Vogliamo utilizzare i falsi a scopo di test. Questo ci permette di testare una classe che dipende da un recupero del database senza realmente connettersi al database.
- Dobbiamo supportare più implementazioni. Ad esempio, potremmo aver bisogno di configurare un sistema che supporti sia i database MySQL che PostgreSQL.
Puoi anche prendere nota dell'inversione dei contenitori di controllo. Questa è una tecnica che ha lo scopo di aiutarti a evitare alberi di costruzione enormi e ingarbugliati che assomigliano a questo pseudocodice:
thing5 = new MyThing5();
thing3 = new MyThing3(thing5, new MyThing10());
myApp = new MyApp(
new MyAppDependency1(thing5, thing3),
new MyAppDependency2(
new Thing1(),
new Thing2(new Thing3(thing5, new Thing4(thing5)))
),
...
new MyAppDependency15(thing5)
);
Ti consente di registrare le tue classi e quindi ti costruisce la costruzione:
injector.register(Thing1); // Yes, you'd need some kind of actual class reference.
injector.register(Thing2);
...
injector.register(MyAppDepdency15);
injector.register(MyApp);
myApp = injector.create(MyApp); // The injector fills in all the construction parameters.
Nota che è più semplice se le classi registrate possono essere singleton single-less .
Parola di cautela
Nota che l'inversione di dipendenza dovrebbe non essere la tua risposta per la logica di disaccoppiamento. Cerca opportunità di utilizzare Parametrizzazione . Considera questo metodo pseudocodice per esempio:
myAverageAboveMin()
{
dbConn = new DbConnection("my connection string");
dbQuery = dbConn.makeQuery();
dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
dbQuery.setParam("min", 5);
dbQuery.Execute();
myData = dbQuery.getAll();
count = 0;
total = 0;
foreach (row in myData)
{
count++;
total += row.x;
}
return total / count;
}
Potremmo usare l'inversione di dipendenza per alcune parti di questo metodo:
class MyQuerier
{
private _dbConn;
MyQueries(dbConn) { this._dbConn = dbConn; }
fetchAboveMin(min)
{
dbQuery = this._dbConn.makeQuery();
dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
dbQuery.setParam("min", min);
dbQuery.Execute();
return dbQuery.getAll();
}
}
class Averager
{
private _querier;
Averager(querier) { this._querier = querier; }
myAverageAboveMin(min)
{
myData = this._querier.fetchAboveMin(min);
count = 0;
total = 0;
foreach (row in myData)
{
count++;
total += row.x;
}
return total / count;
}
Ma non dovremmo, almeno non completamente. Nota che abbiamo creato una classe stateful con Querier
. Ora contiene un riferimento ad alcuni oggetti di connessione essenzialmente globali. Questo crea problemi come la difficoltà a comprendere lo stato generale del programma e il modo in cui le diverse classi si coordinano tra loro. Si noti inoltre che siamo costretti a simulare il querier o la connessione se vogliamo testare la logica della media. Ulteriore approccio migliore sarebbe quello di aumentare la parametrizzazione :
class MyQuerier
{
fetchAboveMin(dbConn, min)
{
dbQuery = dbConn.makeQuery();
dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
dbQuery.setParam("min", min);
dbQuery.Execute();
return dbQuery.getAll();
}
}
class Averager
{
averageData(myData)
{
count = 0;
total = 0;
foreach (row in myData)
{
count++;
total += row.x;
}
return total / count;
}
class StuffDoer
{
private _querier;
private _averager;
StuffDoer(querier, averager)
{
this._querier = querier;
this._averager = averager;
}
myAverageAboveMin(dbConn, min)
{
myData = this._querier.fetchAboveMin(dbConn, min);
return this._averager.averageData(myData);
}
}
E la connessione sarebbe gestita ad un livello ancora più alto che è responsabile per l'operazione nel suo complesso e sa cosa fare con questo output.
Ora possiamo testare la logica della media in modo completamente indipendente dall'interrogazione, e per di più possiamo usarlo in una più ampia varietà di situazioni. Potremmo chiederci se abbiamo anche bisogno degli oggetti MyQuerier
e Averager
, e forse la risposta è che non lo facciamo se non intendiamo il test unitario StuffDoer
, e non il test unitario StuffDoer
sarebbe perfettamente ragionevole dal momento che è così strettamente collegato al database. Potrebbe essere più sensato lasciare che i test di integrazione lo coprano. In tal caso, potremmo fare bene fetchAboveMin
e averageData
in metodi statici.