9546 字
约 31 分钟
1
技术提升篇:什么是依赖注入💉?

技术提升篇:什么是依赖注入💉?

作者:沉默王二 发布时间:2023-02-09T16:16:49.890+0800


微信上有个读者投稿,我一看文章真的是挺不错的,所以在星球里给大家推荐一下,主题叫做:什么是依赖注入💉?

** **内容收录在星球专栏《Java 面试指南》的《技术提升篇》中,专栏托管在语雀平台上(更方便你沉浸式阅读和做笔记),访问地址和密码:https://t.zsxq.com/6iuzn6I

对于 Java 后端开发来说,依赖注入是绕不开的一环,不管是面试中还是日常开发中,基本上都会碰到,希望大家通过这篇内容把依赖注入牢牢地掌握。

这里是读者的 github 地址:https://github.com/dingtingli/

----我们开始

当我们编写 Web 后端代码的时候,会用到这样的代码:

class A {

    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 {

    public A(){        b = new B();    }     public void MethodA(){        b.MethodB();    }}

在 90 年代的时候,代码差不多都是这么写的。这样的代码有什么问题吗?

为了准确地回答这个问题,让我们回到 1995 年。“Bob 大叔”(Robert C. Martin)当年提出了——依赖倒置原则

这个原则有以下两个定义:

  • 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
  • 抽象不应该依赖于细节,细节应该依赖于抽象。

上面的代码很明显不符合这个原则。那么怎样才算能符合这个原则?

依赖倒置原则示例

我们来看看 “Bob 大叔” 在他的著作《敏捷软件开发,原则、模式与实践 C# 版》中的一个示例,来深入理解这个原则的具体含义。

假设有一个控制电水壶(Kettle)温度调节器的软件,该软件可以从一个 I/O 通道中读取当前的温度,并通过向另一个 I/O 通道发送指令来操作电水壶打开或者关闭。

调节器软件将电水壶的温度控制在一个范围(最低温度 和 最高温度之间)。当温度低于最低温度(minTemp)时,就发送指令打开(Turn On)电水壶。当温度高于最高温度(maxTemp)时,就发送指令关闭(Turn Off)电水壶。

根据上述需求,代码可以这样写:

//读取温度的 I/O 通道

//操作电水壶开关的 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 调节器函数接受了两个接口参数:IThermometer 接口可以读取(Read)温度;IHeater 接口可以打开(TurnOn)或者关闭(TurnOff)加热器。

接口的定义和 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 {

    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 {

        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 {

        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 {

    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 {

        b.MethodB();    }} interface IB {    void MethodB();} class B : IB {    public void MethodB(){        //code of method.    }}

第二段代码使用了依赖倒置原则,使得代码在编译阶段的依赖关系发生了反转。Class A 在编译时可以调用 Class B 的抽象 IB 上的方法。而在运行时,Class A 的实例仍然直接调用 Class B 的实例。

依赖倒置编译和运行时的情况

在代码的运行阶段,这两段代码的执行流程是一致的。

因为,在传统的面向对象程序中,执行的代码(主函数)需要先实例化对象、再调用方法,这样代码才能继续执行。

直接依赖 VS 依赖倒置

控制反转介绍

我们回过头来,看看文章最开始使用的代码示例。

代码 3 控制反转

class A {

    public A(IB b){        _b = b;    }     public void MethodA(){        _b.MethodB();    }} interface IB {    void MethodB();} class B : IB {    public void MethodB(){        //code of method.    }}

代码 3 控制反转和代码 2 依赖倒置的结构很类似,所以,很明显代码 3 控制反转也是符合依赖倒置原则的

依赖倒置 VS 控制反转

但这两段代码的使用还是不一样,在使用代码 3 控制反转的项目中,开发人员不需要编写任何实例化 Class B 的代码。

为什么会这样?这时就必须引入控制反转 (IoC Inversion of Control)概念了。

控制反转的概念

控制反转的主要思想是:有一个独立的框架,它可以获得接口 IB 合适的实现类 Class B,并主动创建这个类的实例,再赋值给 Class A 类的一个字段 _b。

如下图所示:

控制反转的运行时的情况

此时,程序执行的控制流程(先实例化对象、再调用方法),就从应用程序本身转移到了 IoC 框架中。也就是说,程序的主要控制者发生了反转,从应用程序变成了 IoC 框架。

从上面的介绍可以看出,框架的一个重要特征是:用户为框架定义的方法,经常会从框架本身,而不是从用户的应用程序代码中调用。

这种控制权的倒置有时被称为好莱坞原则:"不要调用我们,我们会调用你(Don't call me; I'll call you.)"。

在协调和安排应用活动的顺序方面,框架往往扮演着主程序的角色。这种控制的倒置使得框架有能力作为可扩展的骨架。

控制反转的示例

带着上面的理论,我们再来看看 代码 3 控制反转中的代码片段:

class A {

    public A(IB b){        _b = b;    }     public void MethodA(){        _b.MethodB();    }}

代码中,为框架定义的方法——构造函数 public A(IB b) ,会被框架调用而不是应用程序本身调用。

这就是为什么我们在项目中看不到任何调用这个构造函数的原因。

控制反转框架在运行时调用了 Class A 的构造函数,发现参数需要 IB 接口,就找到了接口 IB 合适的实现类 Class B,然后创建了Class B 的实例,最后赋值给构造函数的参数。在这里,程序执行的控制流程完全发生了转变,从应用程序转移到了控制反转框架中。

控制反转的发展也经历了很长时间的迭代:

从 1983 年,Richard E. Sweet 提出好莱坞原则开始;到 1998 年,随着 Java Apache 服务器框架的提出,Stefano Mazzocchi 将控制反转作为框架的主要驱动设计原则之一,普及了这个概念;最后,在 2003 年,Spring、PicoContainer 等框架纷纷实现了控制反转。最终才有了文章最开始展示的那种类型的代码。

3. 依赖注入

依赖注入(DI Dependency Injection)

说了半天,我们还没有提及文章标题中的名词——依赖注入。

2004 年,Martin Fowler 在他的文章《控制反转容器&依赖注入模式》首次提出了依赖注入这个名词。

文章中指出,控制反转这个词太宽泛,并不能很好地解释这个框架的具体实现。作者和 IoC 爱好者们商讨出了一个新的名称:依赖注入(DI Dependency Injection)。

这个名词很形象地解释了控制反转在运行时发生了什么。比如我们之前的代码 3 中,在运行时,构造函数 public A(IB b) 需要接口 IB 的一个实例,此时框架就像是给函数打针一样,注入了 Class B 的实例。

下面这幅漫画生动地展现了用户的使用感受。

依赖注入漫画

打个比方,包饺子的时候我们不需要确定具体是什么馅,只管包就行了,在吃饺子的时候,我说想吃韭菜鸡蛋馅的饺子,这时候就有人用针管给我的饺子注入韭菜鸡蛋馅。

4. 历史演变过程

从上面的描述中我们可以看到,依赖倒置原则是一个软件设计原则,而使用了控制反转的代码都符合这一原则。

控制反转框架,将程序执行的控制流程从应用程序转移到了框架中。最终使用的感觉就是,开发者在代码中所依赖的对象,会在运行的时候直接注入到相应的方法中去,所以就有了一个新名词——依赖注入。

整个技术的演变历程如下:

依赖注入发展历史

我们现在明白了依赖倒置设计原则和控制反转框架的功能,你不觉得控制反转框架很神奇?

微信上有个读者投稿,我一看文章真的是挺不错的,所以在星球里给大家推荐一下,主题叫做:什么是依赖注入💉?

** **内容收录在星球专栏《Java 面试指南》的《技术提升篇》中,专栏托管在语雀平台上(更方便你沉浸式阅读和做笔记),访问地址和密码:https://t.zsxq.com/6iuzn6I

对于 Java 后端开发来说,依赖注入是绕不开的一环,不管是面试中还是日常开发中,基本上都会碰到,希望大家通过这篇内容把依赖注入牢牢地掌握。

这里是读者的 github 地址:https://github.com/dingtingli/

----我们开始

当我们编写 Web 后端代码的时候,会用到这样的代码:

class A {

    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 {

    public A(){        b = new B();    }     public void MethodA(){        b.MethodB();    }}

在 90 年代的时候,代码差不多都是这么写的。这样的代码有什么问题吗?

为了准确地回答这个问题,让我们回到 1995 年。“Bob 大叔”(Robert C. Martin)当年提出了——依赖倒置原则

这个原则有以下两个定义:

  • 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
  • 抽象不应该依赖于细节,细节应该依赖于抽象。

上面的代码很明显不符合这个原则。那么怎样才算能符合这个原则?

依赖倒置原则示例

我们来看看 “Bob 大叔” 在他的著作《敏捷软件开发,原则、模式与实践 C# 版》中的一个示例,来深入理解这个原则的具体含义。

假设有一个控制电水壶(Kettle)温度调节器的软件,该软件可以从一个 I/O 通道中读取当前的温度,并通过向另一个 I/O 通道发送指令来操作电水壶打开或者关闭。

调节器软件将电水壶的温度控制在一个范围(最低温度 和 最高温度之间)。当温度低于最低温度(minTemp)时,就发送指令打开(Turn On)电水壶。当温度高于最高温度(maxTemp)时,就发送指令关闭(Turn Off)电水壶。

根据上述需求,代码可以这样写:

//读取温度的 I/O 通道

//操作电水壶开关的 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 调节器函数接受了两个接口参数:IThermometer 接口可以读取(Read)温度;IHeater 接口可以打开(TurnOn)或者关闭(TurnOff)加热器。

接口的定义和 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 {

    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 {

        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 {

        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 {

    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 {

        b.MethodB();    }} interface IB {    void MethodB();} class B : IB {    public void MethodB(){        //code of method.    }}

第二段代码使用了依赖倒置原则,使得代码在编译阶段的依赖关系发生了反转。Class A 在编译时可以调用 Class B 的抽象 IB 上的方法。而在运行时,Class A 的实例仍然直接调用 Class B 的实例。

依赖倒置编译和运行时的情况

在代码的运行阶段,这两段代码的执行流程是一致的。

因为,在传统的面向对象程序中,执行的代码(主函数)需要先实例化对象、再调用方法,这样代码才能继续执行。

直接依赖 VS 依赖倒置

控制反转介绍

我们回过头来,看看文章最开始使用的代码示例。

代码 3 控制反转

class A {

    public A(IB b){        _b = b;    }     public void MethodA(){        _b.MethodB();    }} interface IB {    void MethodB();} class B : IB {    public void MethodB(){        //code of method.    }}

代码 3 控制反转和代码 2 依赖倒置的结构很类似,所以,很明显代码 3 控制反转也是符合依赖倒置原则的

依赖倒置 VS 控制反转

但这两段代码的使用还是不一样,在使用代码 3 控制反转的项目中,开发人员不需要编写任何实例化 Class B 的代码。

为什么会这样?这时就必须引入控制反转 (IoC Inversion of Control)概念了。

控制反转的概念

控制反转的主要思想是:有一个独立的框架,它可以获得接口 IB 合适的实现类 Class B,并主动创建这个类的实例,再赋值给 Class A 类的一个字段 _b。

如下图所示:

控制反转的运行时的情况

此时,程序执行的控制流程(先实例化对象、再调用方法),就从应用程序本身转移到了 IoC 框架中。也就是说,程序的主要控制者发生了反转,从应用程序变成了 IoC 框架。

从上面的介绍可以看出,框架的一个重要特征是:用户为框架定义的方法,经常会从框架本身,而不是从用户的应用程序代码中调用。

这种控制权的倒置有时被称为好莱坞原则:"不要调用我们,我们会调用你(Don't call me; I'll call you.)"。

在协调和安排应用活动的顺序方面,框架往往扮演着主程序的角色。这种控制的倒置使得框架有能力作为可扩展的骨架。

控制反转的示例

带着上面的理论,我们再来看看 代码 3 控制反转中的代码片段:

class A {

    public A(IB b){        _b = b;    }     public void MethodA(){        _b.MethodB();    }}

代码中,为框架定义的方法——构造函数 public A(IB b) ,会被框架调用而不是应用程序本身调用。

这就是为什么我们在项目中看不到任何调用这个构造函数的原因。

控制反转框架在运行时调用了 Class A 的构造函数,发现参数需要 IB 接口,就找到了接口 IB 合适的实现类 Class B,然后创建了Class B 的实例,最后赋值给构造函数的参数。在这里,程序执行的控制流程完全发生了转变,从应用程序转移到了控制反转框架中。

控制反转的发展也经历了很长时间的迭代:

从 1983 年,Richard E. Sweet 提出好莱坞原则开始;到 1998 年,随着 Java Apache 服务器框架的提出,Stefano Mazzocchi 将控制反转作为框架的主要驱动设计原则之一,普及了这个概念;最后,在 2003 年,Spring、PicoContainer 等框架纷纷实现了控制反转。最终才有了文章最开始展示的那种类型的代码。

3. 依赖注入

依赖注入(DI Dependency Injection)

说了半天,我们还没有提及文章标题中的名词——依赖注入。

2004 年,Martin Fowler 在他的文章《控制反转容器&依赖注入模式》首次提出了依赖注入这个名词。

文章中指出,控制反转这个词太宽泛,并不能很好地解释这个框架的具体实现。作者和 IoC 爱好者们商讨出了一个新的名称:依赖注入(DI Dependency Injection)。

这个名词很形象地解释了控制反转在运行时发生了什么。比如我们之前的代码 3 中,在运行时,构造函数 public A(IB b) 需要接口 IB 的一个实例,此时框架就像是给函数打针一样,注入了 Class B 的实例。

下面这幅漫画生动地展现了用户的使用感受。

依赖注入漫画

打个比方,包饺子的时候我们不需要确定具体是什么馅,只管包就行了,在吃饺子的时候,我说想吃韭菜鸡蛋馅的饺子,这时候就有人用针管给我的饺子注入韭菜鸡蛋馅。

4. 历史演变过程

从上面的描述中我们可以看到,依赖倒置原则是一个软件设计原则,而使用了控制反转的代码都符合这一原则。

控制反转框架,将程序执行的控制流程从应用程序转移到了框架中。最终使用的感觉就是,开发者在代码中所依赖的对象,会在运行的时候直接注入到相应的方法中去,所以就有了一个新名词——依赖注入。

整个技术的演变历程如下:

依赖注入发展历史

我们现在明白了依赖倒置设计原则和控制反转框架的功能,你不觉得控制反转框架很神奇?

微信上有个读者投稿,我一看文章真的是挺不错的,所以在星球里给大家推荐一下,主题叫做:什么是依赖注入💉?

** **内容收录在星球专栏《Java 面试指南》的《技术提升篇》中,专栏托管在语雀平台上(更方便你沉浸式阅读和做笔记),访问地址和密码:https://t.zsxq.com/6iuzn6I

对于 Java 后端开发来说,依赖注入是绕不开的一环,不管是面试中还是日常开发中,基本上都会碰到,希望大家通过这篇内容把依赖注入牢牢地掌握。

这里是读者的 github 地址:https://github.com/dingtingli/

----我们开始

当我们编写 Web 后端代码的时候,会用到这样的代码:

class A {

    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 {

    public A(){        b = new B();    }     public void MethodA(){        b.MethodB();    }}

在 90 年代的时候,代码差不多都是这么写的。这样的代码有什么问题吗?

为了准确地回答这个问题,让我们回到 1995 年。“Bob 大叔”(Robert C. Martin)当年提出了——依赖倒置原则

这个原则有以下两个定义:

  • 高层模块不应该依赖于低层模块,二者都应该依赖于抽象。
  • 抽象不应该依赖于细节,细节应该依赖于抽象。

上面的代码很明显不符合这个原则。那么怎样才算能符合这个原则?

依赖倒置原则示例

我们来看看 “Bob 大叔” 在他的著作《敏捷软件开发,原则、模式与实践 C# 版》中的一个示例,来深入理解这个原则的具体含义。

假设有一个控制电水壶(Kettle)温度调节器的软件,该软件可以从一个 I/O 通道中读取当前的温度,并通过向另一个 I/O 通道发送指令来操作电水壶打开或者关闭。

调节器软件将电水壶的温度控制在一个范围(最低温度 和 最高温度之间)。当温度低于最低温度(minTemp)时,就发送指令打开(Turn On)电水壶。当温度高于最高温度(maxTemp)时,就发送指令关闭(Turn Off)电水壶。

根据上述需求,代码可以这样写:

//读取温度的 I/O 通道

//操作电水壶开关的 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 调节器函数接受了两个接口参数:IThermometer 接口可以读取(Read)温度;IHeater 接口可以打开(TurnOn)或者关闭(TurnOff)加热器。

接口的定义和 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 {

    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 {

        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 {

        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 {

    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 {

        b.MethodB();    }} interface IB {    void MethodB();} class B : IB {    public void MethodB(){        //code of method.    }}

第二段代码使用了依赖倒置原则,使得代码在编译阶段的依赖关系发生了反转。Class A 在编译时可以调用 Class B 的抽象 IB 上的方法。而在运行时,Class A 的实例仍然直接调用 Class B 的实例。

依赖倒置编译和运行时的情况

在代码的运行阶段,这两段代码的执行流程是一致的。

因为,在传统的面向对象程序中,执行的代码(主函数)需要先实例化对象、再调用方法,这样代码才能继续执行。

直接依赖 VS 依赖倒置

控制反转介绍

我们回过头来,看看文章最开始使用的代码示例。

代码 3 控制反转

class A {

    public A(IB b){        _b = b;    }     public void MethodA(){        _b.MethodB();    }} interface IB {    void MethodB();} class B : IB {    public void MethodB(){        //code of method.    }}

代码 3 控制反转和代码 2 依赖倒置的结构很类似,所以,很明显代码 3 控制反转也是符合依赖倒置原则的

依赖倒置 VS 控制反转

但这两段代码的使用还是不一样,在使用代码 3 控制反转的项目中,开发人员不需要编写任何实例化 Class B 的代码。

为什么会这样?这时就必须引入控制反转 (IoC Inversion of Control)概念了。

控制反转的概念

控制反转的主要思想是:有一个独立的框架,它可以获得接口 IB 合适的实现类 Class B,并主动创建这个类的实例,再赋值给 Class A 类的一个字段 _b。

如下图所示:

控制反转的运行时的情况

此时,程序执行的控制流程(先实例化对象、再调用方法),就从应用程序本身转移到了 IoC 框架中。也就是说,程序的主要控制者发生了反转,从应用程序变成了 IoC 框架。

从上面的介绍可以看出,框架的一个重要特征是:用户为框架定义的方法,经常会从框架本身,而不是从用户的应用程序代码中调用。

这种控制权的倒置有时被称为好莱坞原则:"不要调用我们,我们会调用你(Don't call me; I'll call you.)"。

在协调和安排应用活动的顺序方面,框架往往扮演着主程序的角色。这种控制的倒置使得框架有能力作为可扩展的骨架。

控制反转的示例

带着上面的理论,我们再来看看 代码 3 控制反转中的代码片段:

class A {

    public A(IB b){        _b = b;    }     public void MethodA(){        _b.MethodB();    }}

代码中,为框架定义的方法——构造函数 public A(IB b) ,会被框架调用而不是应用程序本身调用。

这就是为什么我们在项目中看不到任何调用这个构造函数的原因。

控制反转框架在运行时调用了 Class A 的构造函数,发现参数需要 IB 接口,就找到了接口 IB 合适的实现类 Class B,然后创建了Class B 的实例,最后赋值给构造函数的参数。在这里,程序执行的控制流程完全发生了转变,从应用程序转移到了控制反转框架中。

控制反转的发展也经历了很长时间的迭代:

从 1983 年,Richard E. Sweet 提出好莱坞原则开始;到 1998 年,随着 Java Apache 服务器框架的提出,Stefano Mazzocchi 将控制反转作为框架的主要驱动设计原则之一,普及了这个概念;最后,在 2003 年,Spring、PicoContainer 等框架纷纷实现了控制反转。最终才有了文章最开始展示的那种类型的代码。

3. 依赖注入

依赖注入(DI Dependency Injection)

说了半天,我们还没有提及文章标题中的名词——依赖注入。

2004 年,Martin Fowler 在他的文章《控制反转容器&依赖注入模式》首次提出了依赖注入这个名词。

文章中指出,控制反转这个词太宽泛,并不能很好地解释这个框架的具体实现。作者和 IoC 爱好者们商讨出了一个新的名称:依赖注入(DI Dependency Injection)。

这个名词很形象地解释了控制反转在运行时发生了什么。比如我们之前的代码 3 中,在运行时,构造函数 public A(IB b) 需要接口 IB 的一个实例,此时框架就像是给函数打针一样,注入了 Class B 的实例。

下面这幅漫画生动地展现了用户的使用感受。

依赖注入漫画

打个比方,包饺子的时候我们不需要确定具体是什么馅,只管包就行了,在吃饺子的时候,我说想吃韭菜鸡蛋馅的饺子,这时候就有人用针管给我的饺子注入韭菜鸡蛋馅。

4. 历史演变过程

从上面的描述中我们可以看到,依赖倒置原则是一个软件设计原则,而使用了控制反转的代码都符合这一原则。

控制反转框架,将程序执行的控制流程从应用程序转移到了框架中。最终使用的感觉就是,开发者在代码中所依赖的对象,会在运行的时候直接注入到相应的方法中去,所以就有了一个新名词——依赖注入。

整个技术的演变历程如下:

依赖注入发展历史

我们现在明白了依赖倒置设计原则和控制反转框架的功能,你不觉得控制反转框架很神奇?

            知识星球
            
    
    
        
        扫码加入星球
        查看更多优质内容
    
    https://wx.zsxq.com/mweb/views/joingroup/join_group.html?group_id=15522885221412
技术提升篇:什么是依赖注入💉?
http://www.clxhxhhr.top/posts/2406/
作者
clxstart
发布于
2026-09-19
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。