Dovresti usare tecniche per risolvere i problemi che sono bravi a risolvere quando hai quei problemi. L'inversione e l'iniezione delle dipendenze non sono diverse.
L'inversione o iniezione di dipendenza è una tecnica che consente al codice di decidere quale implementazione di un metodo viene chiamata in fase di esecuzione. Ciò massimizza i benefici dell'associazione tardiva. La tecnica è necessaria quando la lingua non supporta la sostituzione in fase di esecuzione di 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 è associare il nome a un'altra funzione (riassegnare la variabile che contiene la funzione).
Perché dovremmo voler variare l'implementazione della funzione? Vi sono due ragioni principali:
- Vogliamo usare i falsi a scopo di test. Questo ci consente di testare una classe che dipende da un recupero del database senza effettivamente collegarsi al database.
- Dobbiamo supportare più implementazioni. Ad esempio, potrebbe essere necessario configurare un sistema che supporti sia i database MySQL sia PostgreSQL.
È inoltre possibile prendere nota dell'inversione dei contenitori di controllo. Questa è una tecnica che ti aiuterà a evitare enormi alberi da costruzione aggrovigliati 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 lezioni e quindi fa la costruzione per te:
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.
Si noti che è più semplice se le classi registrate possono essere singoli senza stato .
Avvertenza
Si noti che l'inversione di dipendenza non dovrebbe essere la risposta ideale per la logica di disaccoppiamento. Cerca invece opportunità per usare la 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 del tutto. Si noti che abbiamo creato una classe stateful con Querier
. Ora contiene un riferimento ad alcuni oggetti di connessione essenzialmente globali. Ciò crea problemi come 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 falsificare la query o la connessione se vogliamo testare la logica della media. Ulteriori Un 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 verrebbe gestita ad un livello ancora più alto che è responsabile dell'intera operazione e sa cosa fare con questo output.
Ora possiamo testare la logica della media in modo completamente indipendente dalla query e, inoltre, possiamo usarla in una più ampia varietà di situazioni. Potremmo chiederci se abbiamo persino bisogno degli oggetti MyQuerier
e Averager
, e forse la risposta è che non lo facciamo se non intendiamo test unitari StuffDoer
, e non i test unitari StuffDoer
sarebbero perfettamente ragionevoli dal momento che sono così strettamente accoppiati al database. Potrebbe avere più senso lasciare che i test di integrazione lo coprano. In tal caso, potremmo andare bene fare fetchAboveMin
e averageData
in metodi statici.