当您遇到这些问题时,应该使用技巧来解决它们擅长解决的问题。依赖倒置和注入没有什么不同。
依赖倒置或注入是一种技术,它使您的代码可以决定在运行时调用哪种方法的实现。这使后期绑定的好处最大化。当语言不支持非实例函数的运行时替换时,该技术是必需的。例如,Java缺少将对静态方法的调用替换为对不同实现的调用的机制。与Python相反,替换函数调用所需的全部工作就是将名称绑定到另一个函数(重新分配包含该函数的变量)。
我们为什么要改变功能的实现?主要有两个原因:
- 我们想使用伪造品进行测试。这使我们可以测试依赖于数据库提取的类,而无需实际连接到数据库。
- 我们需要支持多种实现。例如,我们可能需要建立一个同时支持MySQL和PostgreSQL数据库的系统。
您可能还需要注意控制容器的反转。这项技术旨在帮助您避免看起来像此伪代码的大而纠结的构造树:
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)
);
它使您可以注册课程,然后为您完成构建:
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.
请注意,如果注册的类可以是无状态单例,这是最简单的。
注意事项
需要注意的是依赖倒置应该不会是你去到回答去耦逻辑。寻找机会使用参数化。例如,考虑以下伪代码方法:
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;
}
我们可以在此方法的某些部分中使用依赖项反转:
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;
}
但是我们不应该,至少不完全如此。请注意,我们已经使用创建了一个有状态类Querier
。现在,它具有对某些基本全局连接对象的引用。这会产生一些问题,例如难以理解程序的整体状态以及不同类之间如何相互配合。还要注意,如果我们要测试平均逻辑,则必须伪造查询器或连接。更好的方法是增加参数化:
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);
}
}
连接将在更高的级别进行管理,该级别负责整个操作,并且知道如何处理此输出。
现在,我们可以完全独立于查询来测试平均逻辑,而且,我们可以在更广泛的情况下使用它。我们可能会质疑我们是否甚至需要MyQuerier
and Averager
对象,也许的答案是,如果我们不打算进行单元测试StuffDoer
,那么我们就不需要了,而且StuffDoer
由于单元测试与数据库紧密耦合,所以单元测试不是完全合理的。让集成测试覆盖它可能更有意义。在这种情况下,我们可能会精打细算fetchAboveMin
并采用averageData
静态方法。