gball个人知识库
首页
基础组件
基础知识
算法&设计模式
  • 操作手册
  • 数据库
  • 极客时间
  • 每日随笔
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
  • 画图工具 (opens new window)
关于
  • 网盘 (opens new window)
  • 分类
  • 标签
  • 归档
项目
GitHub (opens new window)

ggball

后端界的小学生
首页
基础组件
基础知识
算法&设计模式
  • 操作手册
  • 数据库
  • 极客时间
  • 每日随笔
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
  • 画图工具 (opens new window)
关于
  • 网盘 (opens new window)
  • 分类
  • 标签
  • 归档
项目
GitHub (opens new window)
  • 面试

  • 数据库

  • linux

  • node

  • tensorFlow

  • 基础组件

  • 基础知识

  • 算法与设计模式

  • 分布式

  • 疑难杂症

  • go学习之旅

  • 极客时间

    • 设计模式之美

      • 开篇词 (1讲)

      • 设计模式学习导读 (3讲)

      • 设计原则与思想:面向对象 (11讲)

      • 设计原则与思想:设计原则 (12讲)

        • 理论一:对于单一职责原则,如何判定某个类的职责是否够“单一”?
        • 理论二:如何做到“对扩展开放、修改关闭”?扩展和修改各指什么?
        • 理论三:里式替换(LSP)跟多态有何区别?哪些代码违背了LSP?
        • 理论四:接口隔离原则有哪三种应用?原则中的“接口”该如何理解?
        • 理论五:控制反转、依赖反转、依赖注入,这三者有何区别和联系?
          • 控制反转(IOC)
          • 依赖注入(DI)
          • 依赖注入框架(DIFramework)
          • 依赖反转原则(DIP)
          • 重点回顾
          • 课堂讨论
          • 精选评论
        • 理论六:我为何说KISS、YAGNI原则看似简单,却经常被用错?
        • 理论七:重复的代码就一定违背DRY吗?如何提高代码的复用性?
        • 理论八:如何用迪米特法则(LOD)实现“高内聚、松耦合”?
        • 实战一(上):针对业务系统的开发,如何做需求分析和设计?
        • 实战一(下):如何实现一个遵从设计原则的积分兑换系统?
        • 实战二(上):针对非业务的通用框架开发,如何做需求分析和设计?
        • 实战二(下):如何实现一个支持各种统计规则的性能计数器?
      • 设计原则与思想:规范与重构 (11讲)

      • 设计原则与思想:总结课 (3讲)

      • 设计模式与范式:创建型 (7讲)

      • 设计模式与范式:结构型 (8讲)

      • 设计模式与范式:行为型 (18讲)

      • 设计模式与范式:总结课 (2讲)

      • 开源与项目实战:开源实战 (14讲)

      • 开源与项目实战:项目实战 (9讲)

      • 开源与项目实战:总结课 (2讲)

      • 不定期加餐 (11讲)

      • 结束语 (1讲)

    • Redis核心技术与实战

理论五:控制反转、依赖反转、依赖注入,这三者有何区别和联系?

# 19 | 理论五:控制反转、依赖反转、依赖注入,这三者有何区别和联系?

关于SOLID原则,我们已经学过单一职责、开闭、里式替换、接口隔离这四个原则。今天,我们再来学习最后一个原则:依赖反转原则。在前面几节课中,我们讲到,单一职责原则和开闭原则的原理比较简单,但是,想要在实践中用好却比较难。而今天我们要讲到的依赖反转原则正好相反。这个原则用起来比较简单,但概念理解起来比较难。比如,下面这几个问题,你看看能否清晰地回答出来:

  • “依赖反转”这个概念指的是“谁跟谁”的“什么依赖”被反转了?“反转”两个字该如何理解?

  • 我们还经常听到另外两个概念:“控制反转”和“依赖注入”。这两个概念跟“依赖反转”有什么区别和联系呢?它们说的是同一个事情吗?

  • 如果你熟悉 Java 语言,那 Spring 框架中的 IOC 跟这些概念又有什么关系呢?

看了刚刚这些问题,你是不是有点懵?别担心,今天我会带你将这些问题彻底搞个清楚。之后再有人问你,你就能轻松应对。话不多说,现在就让我们带着这些问题,正式开始今天的学习吧!

# 控制反转(IOC)

在讲“依赖反转原则”之前,我们先讲一讲“控制反转”。控制反转的英文翻译是InversionOfControl,缩写为IOC。此处我要强调一下,如果你是Java工程师的话,暂时别把这个“IOC”跟Spring框架的IOC联系在一起。关于Spring的IOC,我们待会儿还会讲到。

我们先通过一个例子来看一下,什么是控制反转。

public class UserServiceTest {
  public static boolean doTest() {
    // ... 
  }
  
  public static void main(String[] args) {// 这部分逻辑可以放到框架中
    if (doTest()) {
      System.out.println("Test succeed.");
    } else {
      System.out.println("Test failed.");
    }
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14

在上面的代码中,所有的流程都由程序员来控制。如果我们抽象出一个下面这样一个框架,我们再来看,如何利用框架来实现同样的功能。具体的代码实现如下所示:

public abstract class TestCase {
  public void run() {
    if (doTest()) {
      System.out.println("Test succeed.");
    } else {
      System.out.println("Test failed.");
    }
  }
  
  public abstract void doTest();
}

public class JunitApplication {
  private static final List<TestCase> testCases = new ArrayList<>();
  
  public static void register(TestCase testCase) {
    testCases.add(testCase);
  }
  
  public static final void main(String[] args) {
    for (TestCase case: testCases) {
      case.run();
    }
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

把这个简化版本的测试框架引入到工程中之后,我们只需要在框架预留的扩展点,也就是TestCase类中的doTest()抽象函数中,填充具体的测试代码就可以实现之前的功能了,完全不需要写负责执行流程的main()函数了。具体的代码如下所示:

public class UserServiceTest extends TestCase {
  @Override
  public boolean doTest() {
    // ... 
  }
}

// 注册操作还可以通过配置的方式来实现,不需要程序员显示调用 register()
JunitApplication.register(new UserServiceTest();

1
2
3
4
5
6
7
8
9
10

刚刚举的这个例子,就是典型的通过框架来实现“控制反转”的例子。框架提供了一个可扩展的代码骨架,用来组装对象、管理整个执行流程。程序员利用框架进行开发的时候,只需要往预留的扩展点上,添加跟自己业务相关的代码,就可以利用框架来驱动整个程序流程的执行。

这里的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程可以通过框架来控制。流程的控制权从程序员“反转”到了框架。

实际上,实现控制反转的方法有很多,除了刚才例子中所示的类似于模板设计模式的方法之外,还有马上要讲到的依赖注入等方法,所以,控制反转并不是一种具体的实现技巧,而是一个比较笼统的设计思想,一般用来指导框架层面的设计。

# 依赖注入(DI)

接下来,我们再来看依赖注入。依赖注入跟控制反转恰恰相反,它是一种具体的编码技巧。依赖注入的英文翻译是DependencyInjection,缩写为DI。对于这个概念,有一个非常形象的说法,那就是:依赖注入是一个标价25美元,实际上只值5美分的概念。也就是说,这个概念听起来很“高大上”,实际上,理解、应用起来非常简单。

那到底什么是依赖注入呢?我们用一句话来概括就是:不通过new()的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。

我们还是通过一个例子来解释一下。在这个例子中,Notification类负责消息推送,依赖MessageSender类实现推送商品促销、验证码等消息给用户。我们分别用依赖注入和非依赖注入两种方式来实现一下。具体的实现代码如下所示:

// 非依赖注入实现方式
public class Notification {
  private MessageSender messageSender;
  
  public Notification() {
    this.messageSender = new MessageSender(); // 此处有点像 hardcode
  }
  
  public void sendMessage(String cellphone, String message) {
    //... 省略校验逻辑等...
    this.messageSender.send(cellphone, message);
  }
}

public class MessageSender {
  public void send(String cellphone, String message) {
    //....
  }
}
// 使用 Notification
Notification notification = new Notification();

// 依赖注入的实现方式
public class Notification {
  private MessageSender messageSender;
  
  // 通过构造函数将 messageSender 传递进来
  public Notification(MessageSender messageSender) {
    this.messageSender = messageSender;
  }
  
  public void sendMessage(String cellphone, String message) {
    //... 省略校验逻辑等...
    this.messageSender.send(cellphone, message);
  }
}
// 使用 Notification
MessageSender messageSender = new MessageSender();
Notification notification = new Notification(messageSender);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

通过依赖注入的方式来将依赖的类对象传递进来,这样就提高了代码的扩展性,我们可以灵活地替换依赖的类。这一点在我们之前讲“开闭原则”的时候也提到过。当然,上面代码还有继续优化的空间,我们还可以把MessageSender定义成接口,基于接口而非实现编程。改造后的代码如下所示:

public class Notification {
  private MessageSender messageSender;
  
  public Notification(MessageSender messageSender) {
    this.messageSender = messageSender;
  }
  
  public void sendMessage(String cellphone, String message) {
    this.messageSender.send(cellphone, message);
  }
}

public interface MessageSender {
  void send(String cellphone, String message);
}

// 短信发送类
public class SmsSender implements MessageSender {
  @Override
  public void send(String cellphone, String message) {
    //....
  }
}

// 站内信发送类
public class InboxSender implements MessageSender {
  @Override
  public void send(String cellphone, String message) {
    //....
  }
}

// 使用 Notification
MessageSender messageSender = new SmsSender();
Notification notification = new Notification(messageSender);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

实际上,你只需要掌握刚刚举的这个例子,就等于完全掌握了依赖注入。尽管依赖注入非常简单,但却非常有用,在后面的章节中,我们会讲到,它是编写可测试性代码最有效的手段。

# 依赖注入框架(DIFramework)

弄懂了什么是“依赖注入”,我们再来看一下,什么是“依赖注入框架”。我们还是借用刚刚的例子来解释。

在采用依赖注入实现的Notification类中,虽然我们不需要用类似hardcode的方式,在类内部通过new来创建MessageSender对象,但是,这个创建对象、组装(或注入)对象的工作仅仅是被移动到了更上层代码而已,还是需要我们程序员自己来实现。具体代码如下所示:

public class Demo {
  public static final void main(String args[]) {
    MessageSender sender = new SmsSender(); // 创建对象
    Notification notification = new Notification(sender);// 依赖注入
    notification.sendMessage("13918942177", " 短信验证码:2346");
  }
}

1
2
3
4
5
6
7
8

在实际的软件开发中,一些项目可能会涉及几十、上百、甚至几百个类,类对象的创建和依赖注入会变得非常复杂。如果这部分工作都是靠程序员自己写代码来完成,容易出错且开发成本也比较高。而对象创建和依赖注入的工作,本身跟具体的业务无关,我们完全可以抽象成框架来自动完成。

你可能已经猜到,这个框架就是“依赖注入框架”。我们只需要通过依赖注入框架提供的扩展点,简单配置一下所有需要创建的类对象、类与类之间的依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。

实际上,现成的依赖注入框架有很多,比如GoogleGuice、JavaSpring、PicoContainer、ButterflyContainer等。不过,如果你熟悉JavaSpring框架,你可能会说,Spring框架自己声称是控制反转容器(InversionOfControlContainer)。

实际上,这两种说法都没错。只是控制反转容器这种表述是一种非常宽泛的描述,DI依赖注入框架的表述更具体、更有针对性。因为我们前面讲到实现控制反转的方式有很多,除了依赖注入,还有模板模式等,而Spring框架的控制反转主要是通过依赖注入来实现的。不过这点区分并不是很明显,也不是很重要,你稍微了解一下就可以了。

# 依赖反转原则(DIP)

前面讲了控制反转、依赖注入、依赖注入框架,现在,我们来讲一讲今天的主角:依赖反转原则。依赖反转原则的英文翻译是DependencyInversionPrinciple,缩写为DIP。中文翻译有时候也叫依赖倒置原则。

为了追本溯源,我先给出这条原则最原汁原味的英文描述:

我们将它翻译成中文,大概意思就是:高层模块(high-levelmodules)不要依赖低层模块(low-level)。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions)。

所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。在平时的业务代码开发中,高层模块依赖底层模块是没有任何问题的。实际上,这条原则主要还是用来指导框架层面的设计,跟前面讲到的控制反转类似。我们拿Tomcat这个Servlet容器作为例子来解释一下。

Tomcat是运行JavaWeb应用程序的容器。我们编写的Web应用程序代码只需要部署在Tomcat容器下,便可以被Tomcat容器调用执行。按照之前的划分原则,Tomcat就是高层模块,我们编写的Web应用程序代码就是低层模块。Tomcat和应用程序代码之间并没有直接的依赖关系,两者都依赖同一个“抽象”,也就是Sevlet规范。Servlet规范不依赖具体的Tomcat容器和应用程序的实现细节,而Tomcat容器和应用程序依赖Servlet规范。

# 重点回顾

好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要掌握的重点内容。

1.控制反转

实际上,控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计。这里所说的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程通过框架来控制。流程的控制权从程序员“反转”给了框架。

2.依赖注入

依赖注入和控制反转恰恰相反,它是一种具体的编码技巧。我们不通过new的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类来使用。

3.依赖注入框架

我们通过依赖注入框架提供的扩展点,简单配置一下所有需要的类及其类与类之间依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。

4.依赖反转原则

依赖反转原则也叫作依赖倒置原则。这条原则跟控制反转有点类似,主要用来指导框架层面的设计。高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不要依赖具体实现细节,具体实现细节依赖抽象。

# 课堂讨论

从Notification这个例子来看,“基于接口而非实现编程”跟“依赖注入”,看起来非常类似,那它俩有什么区别和联系呢?

欢迎在留言区写下你的答案,和同学一起交流和分享。如果有收获,也欢迎你把这篇文章分享给你的朋友。

# 精选评论

点击查看

小晏子

课后思考: “基于接口而非实现编程”与“依赖注入”的联系是二者都是从外部传入依赖对象而不是在内部去new一个出来。 区别是“基于接口而非实现编程”强调的是“接口”,强调依赖的对象是接口,而不是具体的实现类;而“依赖注入”不强调这个,类或接口都可以,只要是从外部传入不是在内部new出来都可以称为依赖注入。


下雨天

区别: 1.依赖注入是一种具体编程技巧,关注的是对象创建和类之间关系,目的提高了代码的扩展性,我们可以灵活地替换依赖的类。 2.基于接口而非实现编程是一种设计原则,关注抽象和实现,上下游调用稳定性,目的是降低耦合性,提高扩展性。

联系: 都是基于开闭原则思路,提高代码扩展性!


辣么大

1⃣️控制反转是一种编程思想,把控制权交给第三方。依赖注入是实现控制反转最典型的方法。 2⃣️依赖注入(对象)的方式要采用“基于接口而非实现编程”的原则,说白了就是依赖倒转。 3⃣️低层的实现要符合里氏替换原则。子类的可替换性,使得父类模块或依赖于抽象的高层模块无需修改,实现程序的可扩展性。


业余爱好者

原来的模式是一个spring开发的项目放在Tomcat中,控制权在Tomcat手中。现在微服务兴起,大家都用springboot开发。此时是Tomcat在springboot项目当中。控制权在springboot手中,虽然只是表面上。这便是控制反转。

这是一场控制权争夺之战。


KIM

感觉比headfirst设计模式讲的清晰


Ken张云忠

区别: 基于接口而非实现编程:是面向对象编程的一种方式.减少对外部的依赖,还可以提升代码的灵活性,扩展及修改时可以控制风险的传播,符合开闭原则. 依赖注入:是一种具体的编码技巧,属于编程规范的范畴.不通过new的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类来使用。 联系: 两者结合在一起可以实现代码的灵活性,减少对外部的依赖,提升代码的可维护性/可扩展性. 课外感想: 非常喜欢王争老师这样有深度内涵的课程,概念理解深入透彻,宏观方向把握准确,跟着老师的课程更有信心去挑战阿里这样一流企业的工作.报告老师,我归队了.


MindController

深夜打卡


帆大肚子


iLeGeND

有收获


javaadu

课堂讨论:这两个概念没什么关系,讲的不是一个事。依赖注入讲的是一个对象如何获得它运行所依赖的对象,所谓依赖注入就是不需要自己去new,让框架注入进来;基于接口而不是实现编程讲的是抽象思维的应用,利用编程,可以屏蔽掉底层具体实现改变导致上层改变的问题。

文中的那个例子,只是恰好同时使用了依赖注入这个编程技巧,同时也实践了基于接口而非实现编程这个原则。


阿顺


阿冰777

基于接口而非实现编程(依赖倒置原则):高层和低层组件都使用了一样的接口,然后让接口去控制整个逻辑,这样高层组件就不会依赖于具体的低层组件实现。简单来讲,就是大家都用接口,彼此不认识。 依赖注入:依赖注入就是一个组件内部依赖一个对象,但是他不自己造,等别人送上来。 他们俩的关系就是,在依赖倒置原则指导下的设计里,组件都没有内部创造依赖的对象,全是通过外部传入的,但是也不一定是注入,有可能只是个过客(传进去用完就扔),而且注入的时候,都是以接口的形式注入的,而依赖注入并不一定是接口。


再见孙悟空

“基于接口而非实现编程”和“依赖注入”

联系: 都能实现注入功能,程序依赖的对象都能在外部事先创建而无需程序内部显示new。

区别: “基于接口而非实现编程”可以看作是“依赖注入”的一种实现方式。除了构造方法注入外,依赖注入还包括setter方法注入。


空知


睡觉💤

控制反转:控制指的是程序流程的控制,反转是指程序的流程的控制权由程序员转移到框架 依赖注入:上层类依赖底层类执行业务,以前往往将底层类作为上层类的成员变量,在上层类的内部声明底层类。注入就是底层类在外边声明,通过接口的方式注入到上层类中 依赖反转原则:我的理解是模块的解耦。上层模块依赖于低等模块,通过抽象出一套规则或者接口,使得上层业务依赖于抽象规则,低层业务实现规则。

我觉得基于接口编程与依赖反转原则比较类似,区别的话,依赖反转原则,依赖的不一定非得是接口,也许是一套规则,比如老师举的tomcat与java服务的例子


李小四

设计模式_19 #作业 “基于接口而非实现编程”:是一种设计原则。 “依赖注入”:一种对上面原则的应用。

#感想: 今天的内容,一定程度上是对前面基础原则的组合式实践。 在依赖反转原则里, “具体实现依赖抽象”,是对“基于接口而非实现编程”的实践。 比如“高层模块不要依赖低层模块”,是“开闭原则”的实践。

像“单一职责”、“开闭原则”等的底层原则,我们都能理解它的“字面含义”,但真正掌握它要更多的实践和更丰富的信息量,这是我理解的,为什么还要继续讲基于基础原则的实践。


Smallfly

依赖倒置原则概念是高层次模块不依赖于低层次模块。看似在要求高层次模块,实际上是在规范低层次模块的设计。

低层次模块提供的接口要足够的抽象、通用,在设计时需要考虑高层次模块的使用种类和场景。

明明是高层次模块要使用低层次模块,对低层次模块有依赖性。现在反而低层次模块需要根据高层次模块来设计,出现了「倒置」的显现。

这样设计好处有两点:

1.低层次模块更加通用,适用性更广 2.高层次模块没有依赖低层次模块的具体实现,方便低层次模块的替换

思考题:

基于接口而非实现编程,是一种指导编码的思想。依赖注入是它的一种具体应用。

个人理解,仅供参考~


墨雨

感觉依赖反转原则是不是可以叫“依赖抽象原则”?😂,从字面意思来看我觉得可以翻译成:高层模块和低层模块及实现细节都应依赖于抽象。


MarksGui

这个专栏确实讲解的非常细致!争哥确实是用心做专栏!以前对很多类似的概念都没理解透彻,通过这个专栏完全明白了!


秋天


#极客时间
上次更新: 2025/06/04, 15:06:15
理论四:接口隔离原则有哪三种应用?原则中的“接口”该如何理解?
理论六:我为何说KISS、YAGNI原则看似简单,却经常被用错?

← 理论四:接口隔离原则有哪三种应用?原则中的“接口”该如何理解? 理论六:我为何说KISS、YAGNI原则看似简单,却经常被用错?→

最近更新
01
AIIDE
03-07
02
githubActionCICD实战
03-07
03
windows安装Deep-Live-Cam教程
08-11
更多文章>
Theme by Vdoing
总访问量 次 | 总访客数 人
| Copyright © 2021-2025 ggball | 赣ICP备2021008769号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×

评论

  • 评论 ssss
  • 回复
  • 评论 ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss
  • 回复
  • 评论 ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss
  • 回复
×