设计模式学习笔记 - 设计原则 - 5.依赖反转原则(控制反转、依赖反转、依赖注入)

前言

今天学习 SOLID 中的最后一个原则,依赖反转原则。

本章内容,可以带着如下几个问题:

  • “依赖反转” 这个概念指的是 “谁跟谁” 的 “什么依赖” 被反转了? “反转” 这两个字该如何理解。
  • 我们还经常听到另外两个概念:“控制反转” 和 “依赖注入”。这两个概念跟 “依赖反转” 有什么区别和联系吗?它们说的是同一个事情吗?
  • 如果你熟悉 Java 语言,那 Spring 框架中的 IOC 跟这些概念有什么关系?

控制反转(IOC)

在讲依赖反转原则之前,我们先讲下“控制反转”。

如果你是Java工程师的话,暂时别把这个 “IOC” 和 Spring 框架的 “IOC” 联系在一起。关于 Spring 的 IOC ,我们待会还会讲到。

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

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

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

public abstract class TestCase {public void run() {if (doTest()) {System.out.println("Test succeeded.");} 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 testCase : testCases) {testCase.run();}}
}

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

public class UserServiceTest extends TestCase {@Overridepublic void doTest() {// ...}
}// 注册操作还可以通过配置的方式来实现,不需要程序员显示调用 register()
JunitApplication.register(new UserServiceTest());

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

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

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

依赖注入(DI)

依赖注入和控制反转相反,它是一种编码技巧。

依赖注入英文翻译为:Dependency Injection,缩写为 DI。

什么是依赖注入? 用一句话来概括就是:不通过 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.sendMessage(cellPhone, message);}
}public class MessageSender {public void sendMessage(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.sendMessage(cellPhone, message);}
}public class MessageSender {public void sendMessage(String cellPhone, String message) {// ...}
}// 使用Notification
MessageSender messageSender = new MessageSender();
Notification notification = new Notification(messageSender);

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

public class Notification {private MessageSender messageSender;public Notification(MessageSender messageSender) {this.messageSender = messageSender;}public void sendMessage(String cellPhone, String message) {this.messageSender.sendMessage(cellPhone, message);}
}public interface MessageSender {void sendMessage(String cellPhone, String message);
}public class SmsSender implements MessageSender {@Overridepublic void sendMessage(String cellPhone, String message) { /**/ }
}public class InboxSender implements MessageSender {@Overridepublic void sendMessage(String cellPhone, String message) { /**/ }
}// 使用Notification
MessageSender messageSender = new SmsSender();
Notification notification = new Notification(messageSender);

实际上,只需掌握刚刚举的例子,就掌握了依赖注入。尽管依赖注入非常简单,但却非常有用。

依赖注入框架(DI Framework)

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

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

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

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

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

实际上,现成的依赖注入框架有很多,比如 Google Guice、Java Spring、Pico Container 等。

不过,Spring 框架自己声称是控制反转容器(Inversion of Control Container)。
实际上,这两种说法都没错。只是控制反转容器这种表述是一种非常宽泛的描述,除了依赖注入,还有模板模式等,而 Spring 框架的控制反转主要是通过依赖注入来实现的。

依赖反转原则(DIP)

接下来讲一下本章的主角:依赖反转原则。依赖反转原则的英文翻译是 Dependency Inversion Principle,缩写为 DIP。有时也翻译成依赖倒置原则。

英文原文描述:

High-level modules shouldn’t depend on low-level modules。Both modules should depend on abstractions shouldn’t depend on details。Details depend on abstractions.

翻译成中文,大概意思是: 高层模块不要依赖低层模块高层模块和低层模块应该通过抽象来相互依赖。此外,抽象不要依赖具体实现细节具体实现细节依赖抽象

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

Tomcat 是运行 Java Web 应用程序的容器。我们编写 Web 应用程序代码只需要部署在 Tomcat 容器下,便可以被 Tomcat 容器调用执行。

  • 按照之前的划分原则,Tomcat 就是高层模块,我们编写的 Web 应用程序属于低层模块。
  • Tomcat 和 应用程序之间并没有直接的依赖关系,两者依赖同一个抽象,也就是 Servlet 规范。
  • Servlet 规范不依赖具体的 Tomcat 容器和应用程序的实现细节。
  • 而 Tomcat 容器和应用程序依赖 Servlet 规范。

总结

1.控制反转

控制反转是一个比较抽象的设计思想,并不是具体的实现方法,一般指导框架层面的设计。

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

2.依赖注入

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

3.依赖注入框架

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

4.依赖反转原则

依赖反转原则,也叫依赖倒置原则。这条原则跟控制反转有点类似,主要用来指导框架层面的设计。

  • 高层模型不依赖低层模块
  • 它们共同依赖同一个抽象
  • 抽象不要依赖具体实现细节
  • 具体实现细节依赖抽象。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/716284.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【分块三维重建】【slam】LocalRF:逐步优化的局部辐射场鲁棒视图合成(CVPR 2023)

项目地址&#xff1a;https://localrf.github.io/ 题目&#xff1a;Progressively Optimized Local Radiance Fields for Robust View Synthesis 来源&#xff1a;KAIST、National Taiwan University、Meta 、University of Maryland, College Park 提示&#xff1a;文章用了s…

【Spring】20 解析Spring注解驱动的容器配置

文章目录 注解 vs. XMLJavaConfig选项注解配置注解注入顺序注解处理器实际运用总结 Spring 框架一直以 XML 配置为主导&#xff0c;然而随着注解驱动配置的引入&#xff0c;我们不禁思考&#xff1a;是注解配置优于 XML 呢&#xff0c;还是反之&#xff1f;本篇博客将介绍 Spri…

如何将一个远程git的所有分支推到另一个远程分支上

如何将一个远程git的所有分支推到另一个远程分支上 最初有 12 个分支 执行 git remote add 远程名 远程git地址 git push 远程名 --tags "refs/remotes/origin/*:refs/heads/*"之后就变成 26个分支

小项目:2024/3/2

一、TCP机械臂测试 代码&#xff1a; #include <myhead.h> #define SER_IP "192.168.125.254" //服务器端IP #define SER_PORT 8888 //服务器端端口号#define CLI_IP "192.168.199.131" //客户端IP #define CLI_P…

100条数据秒杀,如何避免超卖【待补充更细的资料】

使用Redis预减库存&#xff1a;利用Redis的原子性操作&#xff0c;如DECR命令&#xff0c;来预先减少库存。当商品库存数量在Redis中被减少到0时&#xff0c;后续的请求将被拒绝&#xff0c;从而确保只有限定数量的订单能够进入后续流程。悲观锁&#xff1a;在数据库层面使用悲…

面试笔记系列八之JVM基础知识点整理及常见面试题

目录 类实例化加载顺序 类的实例化顺序 JVM创建对象的过程 JVM的运行机制 直接内存&#xff08;Direct Memory&#xff09; JVM后台运行的线程 JVM 常用参数 标准参数中比较有用的&#xff1a; 非标准参数又称为扩展参数&#xff0c;比较有用的是 非Stable参数 class初…

【DAY07 软考中级备考笔记】数据结构:线性结构,数组矩阵和广义表

数据结构&#xff1a;线性结构&#xff0c;数组矩阵和广义表 3月2日 – 天气&#xff1a;晴 1. 线性表的定义和存储方式 > 这一部分只需要掌握下面的两点即可&#xff1a; > > * 采用顺序存储和链式存储的特点 > * 单链表的插入和删除操作 2. 栈和队列 > 这里需…

35 Spring整合Elasticsearch

文章目录 Spring整合Elasticsearch引入依赖配置Elasticsearch解决冲突 使用ElasticsearchSpring Data Elasticsearch建立映射关系常用方法添加数据修改数据删除数据搜索数据&#xff08;es核心&#xff09;步骤构造搜索条件 并 应用进行查询使用查询结果 Spring整合Elasticsear…

Spring注解之事务 @Transactional

目录 Spring 对事务的支持 事务 Transactional Spring 对事务的支持 提醒一次&#xff1a;你的程序是否支持事务首先取决于数据库 &#xff0c;比如使用 MySQL 的话&#xff0c;如果你选择的是 innodb 引擎&#xff0c;那么恭喜你&#xff0c;是可以支持事务的。但是&#x…

鸿蒙Harmony应用开发—ArkTS声明式开发(通用属性:Popup控制)

给组件绑定popup弹窗&#xff0c;并设置弹窗内容&#xff0c;交互逻辑和显示状态。 说明&#xff1a; 从API Version 7开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 popup弹窗的显示状态在onStateChange事件回调中反馈&#xff0c;其显…

opencv内存溢出del释放变量 (python)

报错&#xff1a; cv2.error: OpenCV(3.4.17) D:\a\opencv-python\opencv-python\opencv\modules\core\src\alloc.cpp:73: error: (-4:Insufficient memory) Failed to allocate 12211548 bytes in function ‘cv::OutOfMemoryError’ 检查内存代码 import psutil# 获取当前进…

内存空间担保机制

什么是内存空间担保机制&#xff1f; 内存空间担保机制&#xff08;Memory Space Guarantee&#xff09;是垃圾回收&#xff08;Garbage Collection&#xff09;算法中的一种策略。它用于在进行垃圾回收过程&#xff08;如Minor GC或Full GC&#xff09;时&#xff0c;确保老年…

Java项目layui分页中文乱码

【问题描述】这部分没改之前中文乱码。 【解决办法】在layui.js或者layui.all.js文件中替换共、页、条转换成Unicode码格式。 字符Unicode共&#x5171页&#x9875条&#x6761【完美解决】改完之后重新运行项目&#xff0c;浏览器F12缓存清除就好了&#xff0c;右键

MySQL的单表和多表查询

我们在前面曾构建过三个用于实验的表格&#xff0c;下面将基于这三个表进行实践。 # 建立一个用于实验的三个表格 mysql> create table emp (-> empno varchar(10),-> ename varchar(50),-> job varchar(50),-> mgr int,-> hiredate timestamp,-&…

课程表系列(BFS)

广度优先搜索 文章目录 广度优先搜索207. 课程表210. 课程表 II思路 630. 课程表 III1462. 课程表 IV547. 省份数量 207. 课程表 207. 课程表 你这个学期必须选修 numCourses 门课程&#xff0c;记为 0 到 numCourses - 1 。 在选修某些课程之前需要一些先修课程。 先修课程…

c++11 标准模板(STL)(std::tuple)(三)

定义于头文件 <tuple> template< class... Types > class tuple; (C11 起) 类模板 std::tuple 是固定大小的异类值汇集。它是 std::pair 的推广。 若 (std::is_trivially_destructible_v<Types> && ...) 为 true &#xff0c;则 tuple 的析构函数是…

【AI绘画】免费GPU Tesla A100 32G算力部署Stable Diffusion

免责声明 在阅读和实践本文提供的内容之前&#xff0c;请注意以下免责声明&#xff1a; 侵权问题: 本文提供的信息仅供学习参考&#xff0c;不用做任何商业用途&#xff0c;如造成侵权&#xff0c;请私信我&#xff0c;我会立即删除&#xff0c;作者不对读者因使用本文所述方法…

Matlab 机器人工具箱 RobotArm类

文章目录 1 RobotArm1.1 方法1.2 注意2 RobotArm.RobotArm3 RobotArm.cmove4 其他官网:Robotics Toolbox - Peter Corke 1 RobotArm 串联机械臂类 1.1 方法 方法描述plot显示机器人的图形表示teach驱动物理和图形机器人mirror使用机器人作为从机来驱动图形</

深入了解Kafka的文件存储原理

Kafka简介 Kafka最初由Linkedin公司开发的分布式、分区的、多副本的、多订阅者的消息系统。它提供了类似于JMS的特性&#xff0c;但是在设计实现上完全不同&#xff0c;此外它并不是JMS规范的实现。kafka对消息保存是根据Topic进行归类&#xff0c;发送消息者称为Producer&…

IntelliJ IDEA 常用的插件

IntelliJ IDEA有很多常用的插件&#xff0c;这些插件可以扩展IDE的功能&#xff0c;提高开发效率。以下是一些常用的插件&#xff1a; Maven Helper&#xff1a;这是一款分析Maven依赖冲突的插件。在没有此插件时&#xff0c;查看Maven的依赖树和检查依赖包冲突可能需要输入命…