软件设计——依赖倒置
目录
博主介绍
💂 个人社区:CSDN全国各地程序猿
🤟作者介绍:中国DBA联盟(ACDU)成员,CSDN全国各地程序猿(媛)聚集地管理员。目前从事工业自动化软件开发工作。擅长C#、Java、机器视觉、底层算法等语言。2019年成立柒月软件工作室,2021年注册苏州凯捷智能科技有限公司
💬如果文章对你有帮助,欢迎关注、点赞、收藏(一键三连)和C#、Halcon、python+opencv、VUE、各大公司面试等一些订阅专栏哦
💅 有任何问题欢迎私信,看到会及时回复
👤 微信号: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框架提供了XML
和Java Config
注解两种方式来告诉Spring
这个IoC容器
,需要管理哪些抽象接口的具体实现。如今XML方式几乎没有多少人用了,注解声明一个Class
是@Bean @Component @Service @Controller @Repository
等等这些的时候,Spring
就把这个类初始化一个单例出来,管理整个声明周期,提供了一些诸如@PostConstruct
@PreDestroy
等钩子用来定制Bean。依赖方直接通过 @Resource ``@Autowired
等注解,或者直接构造器声明,就能拿到一个Bean
的具体实现了。
通常这些Bean
是作为Interface类型
的,这样方便扩展不同的Implementation
,用@Qualified
或按名称注入依赖
,可以选择不同的实现。
Spring
这个IoC容器
管理Bean
的生命周期流程,参考下面这张图:
如何在JavaScript中使用IoC?
其实主流的几个组件化MVVM框架
,Angular
,Vue
,React
,就已经用了依赖注入了,框架本身就是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的语言和框架中运用这一思想的时候,无需实现如此复杂的框架,只要达到依赖倒置的”实现”和”实现”的解耦效果即可。
💫点击直接资料领取💫
这里有各种学习资料还有有有趣好玩的编程项目,更有难寻的各种资源。