写点什么

软件设计——依赖倒置

  • 2022 年 1 月 18 日
  • 本文字数:3728 字

    阅读完需:约 12 分钟

软件设计——依赖倒置

💂 个人主页:苏州程序大白

💂 个人社区:CSDN 全国各地程序猿

🤟作者介绍:中国 DBA 联盟(ACDU)成员,CSDN 全国各地程序猿(媛)聚集地管理员。目前从事工业自动化软件开发工作。擅长 C#、Java、机器视觉、底层算法等语言。2019 年成立柒月软件工作室,2021 年注册苏州凯捷智能科技有限公司

💬如果文章对你有帮助,欢迎关注、点赞、收藏(一键三连)

🎗️ 承接软件 APP、小程序、网站等开发重点行业应用开发(SaaS、PaaS、CRM、HCM、银行核心系统、监管报送平台、系统搭建、人工智能助理)、大数据平台开发、商业智能、App 开发、ERP、云平台、智能终端、产品化解决方案。测试软件产品测试、应用软件测试、测试平台及产品、测试解决方案。运维数据库维护(SQL Server 、Oracle、MySQL)、 操作系统维护(Windows、Linux、Unix 等常用系统)、 服务器硬件设备维护、网络设备维护、 运维管理平台等。运营服务 IT 咨询 、IT 服务、业务流程外包(BPO)、云/基础设施的管理、线上营销、数据采集与标注、内容管理和营销、设计服务、本地化、智能客服、大数据分析等。

💅 有任何问题欢迎私信,看到会及时回复

👤 微信号:stbsl6,微信公众号:苏州程序大白


前言

昨天看到知乎一个问题问“JavaScript中如何使用依赖注入”,正好最近在写软件设计杂谈系列,就顺便以这个问题为例把依赖倒置原则这个 OOP 理论中的重要原则讲一讲。


我们在 Java Spring 中经常听到”依赖注入”和”控制反转”两个术语,他们和”依赖倒置原则”是什么关系呢,这些术语是什么意思呢?

到底什么是依赖注入(DI)和控制反转(IoC)?

DI 和 IoC 是实现依赖倒置原则的具体手段,依赖倒置是面向对象编程(OOP)的产物,一句话解释下依赖倒置原则:


抽象不应该依赖实现,实现也不应该依赖实现,实现应该依赖抽象。


看了感觉摸不到头脑,什么是”抽象”、”实现”?


举个通俗的例子:


假设你想去吃一碗牛肉面。


如果按照面向过程编程的思维,大概是这样的:


  • 输入:面粉、牛肉、辣椒酱 ;

  • 制作牛肉面,你要按菜谱一步一步做;

  • 输出:牛肉面。


如果你不想自己做,那就按面向对象编程的思维,大概是这样的:


  • 你是一个 Object,现在需要一碗牛肉面;

  • “你”需要一个依赖厨师 Object,因为厨师有”制作牛肉面”这个方法,于是你雇了一个厨师;

  • 你又调用了”超市 Object”的购买方法,买到了 面粉、牛肉、辣椒酱;

  • 跟厨师说你要的口味,把买到的食材给厨师,调用厨师 Object 的”制作牛肉面”方法,完成制作。


这样你们又没发现哪里有问题吗?


  • 我为了吃一碗牛肉面还要雇一个厨师?

  • 我雇了厨师还要自己买食材?


问题在于,”我”这个 Object 依赖了一个厨师 Object,这个就叫”实现”依赖了”实现“。因为依赖了具体的”实现”,所以很多细节被暴露出来了,于是我试图把更多本不该我管的细节(买食材)传递给了具体的”实现”(厨师)。


吃牛肉面的解决之道,不是雇一个厨师,而是去一个面馆,在面馆里看着菜单:”一份微辣大份牛肉面,谢谢”。


这里,”菜单”就是”抽象””厨师Object”就是”实现”。


“我”这个 Object 只需要依赖”菜单”提供的抽象接口,调用”下单”就能吃到牛肉面,而不关心背后的厨师是哪些,他们怎么买的食材,具体是怎么做出来的。这就叫”实现应该依赖抽象“


如果”我”这个Object如果依赖了厨师Object,调用了 new Cook(),就必然要管理这个厨师从初始化到解雇的整个流程了。


也就是说当我调用 new 的瞬间之后:对象完整的生命周期、资源如何创建和销毁全都要我去管了。


但实际上按照下馆子的方式,厨师是餐馆管理的,这一点非常关键:


  • 餐馆就是那个控制反转(IoC)容器,总要有一个东西来管理这些抽象的具体实现,比如餐馆对内管理了数十个不同的厨师,对外提供 10 个菜品。

  • 餐馆给”我”这个 Object”注入”菜单的过程,就是依赖注入(DI)

  • 我应该依赖 抽象的”菜单” 去下单,而不是试图把食材递给厨师张三看着他做,这就是依赖倒置原则。


对比 面向过程、初级面向对象、符合依赖倒置原则的面向对象 这三个方式,我们发现事情似乎变简单了,我不用自己买食材做面条,直接下馆子就 OK 了,这就是面向对象编程的封装和信息隐藏的力量。做牛肉面的复杂度并没有被降低,但整个流程和”我”这个 Object 的耦合解开了。


再回到之前对依赖倒置原则的解释:


抽象不应该依赖实现,实现也不应该依赖实现,实现应该依赖抽象。
复制代码


我们换成 厨师 菜单 客人:


菜单不应该依赖厨师,客人也不应该依赖厨师,客人应该依赖菜单
复制代码


这下一下子都清晰很多吧。


我这里刻意避开类(Class)这个概念,是为了说明 OOP 的思维并不一定要”类”这个概念,重点在于通过信息隐藏来解耦,让复杂的软件系统可以分而治之。

Java Spring 中的 DI 和 IoC

Spring 框架提供了XMLJava Config注解两种方式来告诉Spring这个IoC容器,需要管理哪些抽象接口的具体实现。如今 XML 方式几乎没有多少人用了,注解声明一个Class@Bean @Component @Service @Controller @Repository等等这些的时候,Spring就把这个类初始化一个单例出来,管理整个声明周期,提供了一些诸如 @PostConstruct @PreDestroy等钩子用来定制 Bean。依赖方直接通过 @Resource ``@Autowired 等注解,或者直接构造器声明,就能拿到一个Bean的具体实现了。


通常这些Bean是作为Interface类型的,这样方便扩展不同的Implementation,用@Qualified或按名称注入依赖,可以选择不同的实现。


<font color=#03a9f4 size=5 face= "楷体">Spring这个IoC容器管理Bean的生命周期流程,参考下面这张图:<font>


如何在 JavaScript 中使用 IoC?

其实主流的几个组件化MVVM框架AngularVueReact,就已经用了依赖注入了,框架本身就是IoC容器


不知庐山真面目,只缘身在此山中。


以 Vue 为例:


  • 我们在组件中用”components“声明依赖的组件时,也是一种依赖注入。也许有人说,注入的明明是具体的组件”实现”而不是”抽象”啊? 组件 B 依赖组件 A,但在组件 B 中根本没有去 new 组件 A,也没有管 A 什么时候创建,什么时候销毁,需要怎么初始化,只是为了告诉Vue这个IoC容器:组件 B 依赖组件 A 这个事情,组件的 A 的 init compile mount destroy 这些具体的流程和实现的管理不需要 B 去关心,因此这个声明可以看做是依赖了 A 的”抽象”。这里的”抽象”并不一定是类似Java的``”Interface”这种形式。

  • 控制反转(IoC)容器,就是统一管理各个实现如何初始化、从生到死整个过程的超级管家,Vue 框架本身就干了这个事情,当你用Vue.component Vue.use把组件注册到 Vue 里面的时候,这个组件的实例什么时候挂载到什么地方,都可以看作由 Vue 这个 IoC 容器来控制的。

  • 上面说 Vue 的父子组件之间直接声明components是一种依赖注入,还有一个更明显的inject provide直接给所有后代组件都注入依赖。同样,inject/provide注入给子孙后代组件,这些后代也不用管祖先组件是怎么创建和销毁的。


Angular 从 1.x 的 AngularJS,在参数中直接传递依赖组件的字符串,到后来新的 Angular 框架,都具有非常明显的 IoC 和 DI 的特征。而 require.js 这类工具解决的不是对象与对象之间的耦合问题,所以不完全算依赖注入和控制反转。


另一个非前端的例子,Node.js 服务端框架 nest.js,和 Java Spring 以及 Angular 的用法非常类似,可以阅读官方文档,也有对 IoC 和 DI 的解释和具体使用示例,讲的非常详尽。


具体看本文章


因此,如果项目相对复杂,开始用这些前后端框架,构造器代码中很少 new 非 DTO/VO/PO 对象出来的时候,就已经在欢快地使用依赖注入了,而 IoC 容器就是那个为你管理这些具体实现对象的生与死的幕后 Boss。

依赖注入的问题和局限性

依赖注入一定是”好的模式”吗?


不完全是。今天我去餐馆说要一份不辣的牛肉面,结果上来一份巨辣无比的牛肉面。这就是”信息隐藏”的代价。在 Java 中,SpringBoot已经把 IoC 和 DI 发展的淋漓尽致了,一个 @EnableAutoConfiguration 注解,背后做了很多黑箱的事情,各种约定式的配置直接告诉 Spring 容器该做什么事情,甚至无需写一行代码。物极必反,这样反而让项目容易出现过多冗余的依赖、大量被 Spring 容器中的 Bean 在背后难以控制、一个接口存在过多的实现类、不确定的互相影响、依赖加载顺序问题等等。


虽然可能存在这些问题,但我觉得在以面向对象编程为主的复杂系统引入 IoC 容器和 DI 仍然是有必要的,上述这些问题也有办法避免或解决。让对象自己管理所依赖对象的生命周期,就像直接雇一个厨师来做牛肉面一样简单粗暴,但更容易违背迪米特法则等其他 OOP 的理念,项目的可扩展性和可维护性会受到更强的制约。这里前提是 OOP 情况下的建议,当然 OOP 也有一些局限性,不一定非要用 OOP 作为编程范式。


另一个场景,如果只是一些简单的页面或服务,没有复杂的组件/服务之间的交互,是没有必要为了用 DI 而用 DI 的。

结尾

依赖注入(DI)和控制反转(IoC)是具体的手段,是 OOP 理论中依赖倒置原则的体现形式,通过信息隐藏来降低对象之间的耦合,这就是依赖倒置解决的问题。这种思想的运用不限于语言和框架。像 Java Spring 用工厂/模板方法/代理/单例模式、、注解、反射、动态代理这一系列设计模式和相关技术实现了 IoC 容器,而在没有类似 Spring 的语言和框架中运用这一思想的时候,无需实现如此复杂的框架,只要达到依赖倒置的”实现”和”实现”的解耦效果即可。

💫点击直接资料领取💫


这里有各种学习资料还有有有趣好玩的编程项目,更有难寻的各种资源。


❤️关注苏州程序大白公众号❤️


👇 👇👇

用户头像

苏州凯捷智能科技有限公司 2021.12.21 加入

2019年成立柒月软件工作室,2021年成立苏州凯捷智能科技有限公司。中国DBA联盟(ACDU)成员,CSDN全国各地程序猿(媛)聚集地管理员。目前从事工业自动化软件开发工作。擅长C#、Java、机器视觉、底层算法等语言。

评论

发布
暂无评论
软件设计——依赖倒置