什么是依赖注入?
The following article is from dingtingli Author dingtingli
点击关注公众号,Java干货及时送达👇
当我们编写 Web 后端代码的时候,会用到这样的代码:
class A {
private IB _b;
public A(IB b){
_b = b;
}
public void MethodA(){
_b.MethodB();
}
}
在 Class A
中没有任何地方 new Class B
的实例,但是运行的时候,MethodA
中的变量 _b
已经是 Class B
的一个实例了,为什么会这样?
今天我们就带着疑问,了解一下依赖注入的来龙去脉。
文章从依赖注入的历史出发,分为三个部分:
依赖倒置原则 控制反转 依赖注入
1.依赖倒置原则
依赖倒置原则(DIP Dependency Inversion Principle)
在没有依赖注入的情况下,如果 Class A
调用了 Class B
的方法,这就意味着 Class A
依赖于 Class B
。换句话说,在编译时 Class A
将取决于 Class B
。
class A {
private B b;
public A(){
b = new B();
}
public void MethodA(){
b.MethodB();
}
}
为了准确地回答这个问题,让我们回到 1995 年。“Bob 大叔”(Robert C. Martin)当年提出了——依赖倒置原则。
这个原则有以下两个定义:
高层模块不应该依赖于低层模块,二者都应该依赖于抽象。 抽象不应该依赖于细节,细节应该依赖于抽象。
依赖倒置原则示例
我们来看看 “Bob 大叔” 在他的著作《敏捷软件开发,原则、模式与实践 C# 版》中的一个示例,来深入理解这个原则的具体含义。
假设有一个控制电水壶(Kettle)温度调节器的软件,该软件可以从一个 I/O 通道中读取当前的温度,并通过向另一个 I/O 通道发送指令来操作电水壶打开或者关闭。
调节器软件将电水壶的温度控制在一个范围(最低温度 和 最高温度之间)。当温度低于最低温度(minTemp)时,就发送指令打开(Turn On)电水壶。当温度高于最高温度(maxTemp)时,就发送指令关闭(Turn Off)电水壶。
根据上述需求,代码可以这样写:
//读取温度的 I/O 通道
const byte TERMOMETER = 0x86;
//操作电水壶开关的 I/O 通道
const byte KETTLE = 0x87;
// 开电水壶的指令
const byte TURNON = 1;
//关电水壶的指令
const byte TURNOFF = 0;
//温度调节器函数
void Regulate(double minTemp, double maxTemp)
{
for(;;)
{
//当温度高于最低温度时,就等待 1 秒中,继续循环。
while(in(TERMOMETER) > minTemp)
wait(1);
//否则就发送指令打开电水壶。
out(KETTLE,TURNON);
//当温度低于最高温度时,就等待 1 秒中,继续循环。
while(in(TERMOMETER) < maxTemp)
wait(1);
//否则就发送指令关闭电水壶。
out(KETTLE,TURNOFF);
}
}
in
和 out
函数都是系统底层函数。如果其他类型的加热器(Heater)也有同样的调节温度需求,这段代码会因为包括了电水壶的底层细节无法被重用。
如何修改这段代码让它可以重用?这时候就可以使用依赖倒置原则。
接口的定义和 Regulate 调节器函数都属于高层模块,函数只需要知道着这两个接口,跟具体加热器的实现细节无关。
所有的加热器只需实现这两个接口就可以,这些接口的实现属于底层模块。
这就是依赖关系倒置,高层的 Regulate 调节器函数,不再依赖任何加热器的底层细节,函数本身有了很好的可用性。
最终 Regulate 调节器函数可以写成下面这样:
void Regulate(IThermometer t, IHeater h,
double minTemp, double maxTemp)
{
for(;;)
{
while(t.Read() > minTemp)
wait(1);
h.TurnOn();
while(t.Read() > maxTemp)
wait(1);
h.TurnOff();
}
}
使用依赖倒置原则优化代码
依赖倒置原则,不仅解释了为什么之前代码的写法不好,而且提出了解决方案。
让我们再次回到之前的例子中:
代码 1 直接依赖:
class A {
private B b;
public A(){
b = new B();
}
public void MethodA(){
b.MethodB();
}
}
class B {
public void MethodB(){
//code of method.
}
}
Class A
依赖于 Class B
。如果 Class A
是高层模块,如何让 Class A
不依赖于 Class B
?根据依赖倒置原则,我们可以让 Class A
依赖于 Class B
的抽象 IB
。
代码 2 依赖倒置:
class A {
public void MethodA(IB b){
b.MethodB();
}
}
interface IB {
void MethodB();
}
class B : IB {
public void MethodB(){
//code of method.
}
}
Class A
和 Class B
的依赖关系反转了。Class A
和接口 IB
属于高层模块,Class B
作为接口 IB
的实现属于底层模块。
但是想要调用 Class A
中的 MethodA
,应用程序仍然需要先 new 一个 Class B
的实例。
class Test {
static void Main(){
A a = new A();
B b = new B();
a.MethodA(b);
}
}
这样的调用关系,在编译时 Class A
依赖于抽象 IB
;在运行时,实例 a
仍然直接调用了实例 b
,所以应用程序需要事先准备好 Class B
的实例 b
。
这跟我们说的依赖注入有什么关系?让我们带着这个疑问,先进入下一个概念——控制反转 (IoC Inversion of Control)。
2. 控制反转
控制反转 (IoC Inversion of Control)
直接依赖和依赖倒置运行时的情况
我们回过头来,再看看之前的两段代码。
代码 1 直接依赖:
class A {
private B b;
public A(){
b = new B();
}
public void MethodA(){
b.MethodB();
}
}
class B {
public void MethodB(){
//code of method.
}
}
第一段代码使用了直接依赖的方式,Class A
依赖于 Class B
。编译时依赖关系顺着运行时执行的方向流动,二者方向是一致的。
代码 2 依赖倒置:
class A {
public void MethodA(IB b){
b.MethodB();
}
}
interface IB {
void MethodB();
}
class B : IB {
public void MethodB(){
//code of method.
}
}
第二段代码使用了依赖倒置原则,使得代码在编译阶段的依赖关系发生了反转。Class A
在编译时可以调用 Class B
的抽象 IB
上的方法。而在运行时,Class A
的实例仍然直接调用 Class B
的实例。
在代码的运行阶段,这两段代码的执行流程是一致的。
因为,在传统的面向对象程序中,执行的代码(主函数)需要先实例化对象、再调用方法,这样代码才能继续执行。