Debes usar técnicas para resolver los problemas que son buenos para resolver cuando tienes esos problemas. La inversión de dependencia y la inyección no son diferentes.
La inversión o inyección de dependencia es una técnica que le permite a su código decidir qué implementación de un método se llama en tiempo de ejecución. Esto maximiza los beneficios de la unión tardía. La técnica es necesaria cuando el lenguaje no admite el reemplazo en tiempo de ejecución de funciones que no son de instancia. Por ejemplo, Java carece de un mecanismo para reemplazar las llamadas a un método estático con llamadas a una implementación diferente; contraste con Python, donde todo lo que es necesario para reemplazar la llamada a la función es vincular el nombre a una función diferente (reasignar la variable que contiene la función).
¿Por qué querríamos variar la implementación de la función? Hay dos razones principales:
- Queremos usar falsificaciones con fines de prueba. Esto nos permite probar una clase que depende de una búsqueda de la base de datos sin conectarse realmente a la base de datos.
- Necesitamos soportar múltiples implementaciones. Por ejemplo, podríamos necesitar configurar un sistema que sea compatible con las bases de datos MySQL y PostgreSQL.
También puede tomar nota de la inversión de los contenedores de control. Esta es una técnica destinada a ayudarlo a evitar árboles de construcción enormes y enredados que se parecen a este pseudocódigo:
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)
);
Te permite registrar tus clases y luego hace la construcción por ti:
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.
Tenga en cuenta que es más simple si las clases registradas pueden ser singletons sin estado .
Palabra de precaución
Tenga en cuenta que la inversión de dependencia no debería ser su respuesta a la lógica de desacoplamiento. Busque oportunidades para utilizar la parametrización en su lugar. Considere este método de pseudocódigo, por ejemplo:
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;
}
Podríamos usar la inversión de dependencia para algunas partes de este método:
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;
}
Pero no deberíamos, al menos no completamente. Tenga en cuenta que hemos creado una clase con estado con Querier
. Ahora contiene una referencia a algún objeto de conexión esencialmente global. Esto crea problemas como la dificultad para comprender el estado general del programa y cómo las diferentes clases se coordinan entre sí. Tenga en cuenta también que estamos obligados a falsificar el interrogador o la conexión si queremos probar la lógica de promedio. Además, un mejor enfoque sería aumentar la parametrización :
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);
}
}
Y la conexión se administraría a un nivel aún mayor que es responsable de la operación en su conjunto y sabe qué hacer con esta salida.
Ahora podemos probar la lógica de promedios completamente independiente de la consulta, y además, podemos usarla en una variedad más amplia de situaciones. Podríamos preguntarnos si incluso necesitamos los objetos MyQuerier
y Averager
, y tal vez la respuesta es que no lo hacemos si no pretendemos realizar una prueba unitaria StuffDoer
, y no la prueba unitaria StuffDoer
sería perfectamente razonable ya que está tan estrechamente acoplada a la base de datos. Puede tener más sentido dejar que las pruebas de integración lo cubran. En ese caso, podría estar haciendo bien fetchAboveMin
y averageData
en los métodos estáticos.