面向对象设计原则(五):依赖倒置原则(DIP)
依赖倒置原则(Dependency Inversion Principle,DIP)也称依赖反转原则,是面向对象设计(OOD)中比较重要、常见的一种,下面来总结依赖倒置原则的知识点,包括:
1、什么是依赖倒置原则?
2、为什么需要遵守依赖倒置原则?
3、在面向对象设计中如何实现依赖倒置原则?
4、依赖倒置原则的实例应用(包括面向对象程序设计、系统架构、社会活动中的应用)。
1、什么是依赖倒置原则
依赖倒置原则(Dependency Inversion Principle,DIP)的定义可以总结为以下两点:
a. 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。
b. 抽象不应该依赖于细节;细节应该依赖于抽象。
2、为什么需要遵守依赖倒置原则
依赖倒置原则是一个应用广泛的原则,不仅在面向对象程序框架设计中(是核心原则),还是在架构系统中, 甚至是在社会活动构建组织等方面,都发挥重要作用。
所以,高层、低层、抽象层可以表示为多种意义:
高层:客户端、服务消费者、方法调用者……(可能变动、依赖性高)
低层:服务端、服务提供者、方法实现者,细节实现者……(易变动、通常需要扩展)
抽象:交互行为、策略、契约、流程、业务模型……(稳定、可重用)
下图展示了从传统系统到依赖倒置系统的架构演变(点击查看大图):
1、传统系统
高层模块直接调用易变动的低层模块。
优点:
实现难度小,方便快捷。
缺点:
没有抽象,耦合度高;当低层模块变动时,高层模块也得变动;
高层模块过度依赖低层模块,很难扩展。
而且这种依赖关系具有传递性,即如果是多层次的调用,最低层改动会影响较高层……直到最高层。
2、常见的面向抽象系统
高层模块依赖低层模块提供的抽象,低层模块实现抽象。
优点:
这是通常意义的面向接口编程,或开闭原则;
抽象接口就是低层模块可对外提供的服务。
一定程度上降低了耦合,使得低层模块易于扩展。
缺点:
高层模块还是依赖低层模块,只不过依赖的是低层抽象。
低层抽象还是有较高变动性,而且还是会传递;
使得高层模块的可重用性降低。
3、依赖倒置:第一层境界
高层模块为它所需要的服务声明一个抽象接口,低层模块实现了这些抽象接口。
即:低层模块依赖高层模块,依赖倒置了。
优点:
首先,还是具有前面"常见的面向抽象系统"的优点:降低了耦合,使得低层模块易于扩展。
另外,依赖倒置后,高层模块完全不依赖低层模块,不受低层模块易变性影响,增加高略层的可重用性。
还有,低层模块为高层模块而生,更符合"先有需求,后去实现";而不是低层模块的"假设",这种"假设"通常就是定义说的"抽象依赖细节"。
缺点:
低层模块依赖高层模块,受到高层模块的抽象的限制和变动影响,可重用性低。
另外,高层模块只能使用实现其抽象的低层模块。
4、依赖倒置:第二层境界
把高层模块和低层模块交互行为抽象出来,做到互不依赖,都依赖于提取出来的抽象。
即:无论高层模块或低层模块实现的细节,都依赖于抽象。
优点:
高层模块和低层模块彻底解耦,都很容易实现扩展;
而抽象模块具有很高的稳定性、可重用性,对高/低层模块来说才是真正"可依赖的"。
缺点:
增加了一层抽象层,增加实现难度;
对一些简单的调用关系来说,可能是得不偿失的。
对一些稳定的调用关系,反而增加复杂度,是不正确的。
5、小结
每种实现都有其优点和缺点,都有其存在的理由;如果我们认识到这些,在设计就能作出更合理的选择。
而依赖倒置原则,是构建大型、易扩展、可重用框架/系统的核心原则。
一般来说,系统中存在违反依赖倒置原则的地方,很可能就是我们需要优化的地方。
3、在面向对象设计中如何实现依赖倒置原则
前面概括的说明了依赖倒置原则的应用,下面更具体的说明:依赖倒置原则在面向对象设计方面需要怎么做?如下图(点击查看大图):
1、基础:依赖于抽象
依赖倒置原则在面向对象框架设计的基础规则:
依赖于抽象。
即:
程序中所有的依赖关系都应该终止于抽象类或者接口中;
而不应该依赖于具体类。
根据这个启发式规则,编程时可以这样做:
(1)、类中的所有成员变量必须是接口或抽象,不应该持有一个指向具体类的引用或指针。
即所有具体类只能通过接口或抽象类连接。
不应该:HashMap map;
应该:Map map;
(3)、任何类都不应该从具体类派生。
(4)、任何方法都不应该覆写它的任何基类中已经实现的方法。(里氏替换原则)
(5)、任何变量实例化都需要实现创建模式(如:工厂方法/模式),或使用依赖注入框架(如:Spring IOC)。
但通常都会违反该启发规则的情况,比如Java中可以依赖稳定的String类,而不造成损害。
而我们自己编写的大多数具体类都是不稳定的,通过把它们隐藏在抽象接口的后面,可以隔离它们的不稳定性。
2、核心:依赖倒置
虽然抽象接口可以隐藏隔离不稳定性,但类接口必须变化时,还是会破坏抽象接口的隔离性。
所以,可以由客户类来声明它们需要的服务接口,改变实现抽象接口的类就不会影响到客户了。
依赖倒置原则第一层境界:
在这种实现中,高级组件和低级组件分布到单独的软件包/库中,其中定义高级组件所需的行为/服务的接口由高级组件的库所有并存在于高级组件的库中。
通过低级组件实现高级组件的接口,要求低级组件包依赖于编译的高级组件,从而颠倒了常规的依赖关系。
在这个版本的DIP中,较低层组件对较高级别层的接口的依赖使得较低层组件的重新利用变得困难。
这种实现反而将"传统的依赖关系从上到下颠倒",从底层到顶端相反。
依赖倒置原则第二层境界:
将所有层分离成自己的包,更灵活,获得更好的扩展性、重用性、鲁棒性和移植性。
注:
有些书籍和文章把DIP等同于面向接口编程;
个人理解是面向接口编程只是DIP的基础,而核心是"依赖倒置"。
4、依赖倒置原则的实例应用
前面也说到依赖倒置原则是一个应用广泛的原则,不仅在面向对象程序框架设计中(是核心原则),还是在架构系统中, 甚至是在社会活动构建组织等方面,都发挥重要作用。
4-1、Button控制Lamp的案例
1、通常的方案
Button事件响应函数中直接调用Lamp相应动作函数。
这个方案违反了DIP,当Lamp类改变时,Button类会受到影响。
2、依赖倒置第一境界的方案
首先,找出潜在的抽象:
它是应用背后的抽象,是那些不随具体细节的改变而改变的真理。
它是系统内部的系统——它是隐喻(metaphore)。
这个例子背后的抽象是:
检测用户的开/关指令并将指令传给目标对象。
所以,可以有下面这个方案:
Button关联一个其可以做的控制动作的抽象接口ButtonServer,Lamp实现这个接口。
优点:
这样可以使Button控制那些愿意实现ButtonServer接口的任何设备;
获得了极大的灵活性,也意味着Button将能够控制还没有创造出来的对象。
缺点:
存在一个不友好的约束,需要实现该接口才能被Button控制;
但还可能被其他不同于Button的对象控制。
3、依赖倒置第二境界的方案
上面的方案:Lamp依赖于ButtonServer,但ButtonServer没有依赖于Button。
对此,还可以进一步改进,Button和ButtonServer可以分开在不同的库中,ButtonServer改名为SwitchableDevice;
这样SwitchableDevice的使用就不必包含Button的使用。
这种方案总结:
接口没有所有者,可以被许多不同的客户使用,并被许多不同的服务都实现。
接口可以被放置在一个单独的组中,如在Java放在一个单独的package中。
4、小结
上面可以看到应用DIP带来的好处:解耦合、易扩展、可重用等。
相似的,DIP可以应用于任何存在一个类向另一个类发送消息的地方。
但是就这个例子"Button控制Lamp"来说,实际中需要考虑:是不是值得应用DIP。
4-2、DIP在系统架构中的应用
1、数据存储
传统:
业务层在不同数据类型的CURD地方,直接调用不同数据系统(Mysql/Mongo/Rides缓存/本地文件系统)的相关接口,把数据保存到相应系统。
DIP:
抽象数据存储层(DAO),DAO模块实现不同数据系统的接口;
业务层调用DAO层接口传入不同类型数据,DAO层适配到相应数据系统。
2、消息传输
通常:
上层通过调用下层接口,发消息给下层。
一般情况上层需要同步等待下层处理后的响应。
DIP:
通过消息队列(Message Queue)解耦,实现异步传输;
上层调用MQ的发送消息接口,消息发送到MQ,下层调用MQ获取消息的接口进行消费;
这时上/下层都可以轻松进行扩展。
注意:如果上层实时关注下层的处理结果,MQ就不适用。
3、配置/服务注册中心
传统:
下层服务配置保存在配置文件或数据库,改动一些配置或扩展下层服务时可能影响到上层服务;
如:
上层服务依赖下层服务的IP、RPC服务地址等等。
DIP:
独立出来配置/服务注册中心;
当下层服务配置改变时,更新到配置心中;配置中心把更新推送到上层服务。
如:
针对IP,可以通过DNS,上层使用的是域名,下层服务IP更新时,只需在DNS改动记录指向新的IP。
针对RPC服务地址,通过服务注册中心(如:ZooKeeper)来发布;上层通过注册中心接口订阅所需服务,当下层通过注册中心接口扩展新注册服务或更新服务地址时,上层服务会收到订阅的服务更新情况。
4-3、DIP在社会活动中的应用
1、电脑主板
传统:
开始的电脑,各组件(内存、显卡等)根据某个厂家的产品直接焊接集成到一起。
DIP:
各组件都定义标准接口,主板上预留标准接口,各厂家按标准接口来生产。
这样就很容易更换不同厂家的产品。
2、DIP公司
传统:
客户直接到某个商家消费,或用户直接使某个商家的服务。
DIP:
一些公司构建中间平台,众多商家把自家产品或服务放到平台上,客户通过该平台很容易找到所需的某个产品或服务。
所谓:一流的企业做标准(中间抽象层),二流的企业做品牌(高层),三流的企业做产品(下层);
DIP公司就是一流的企业,如:阿里巴巴/淘宝、支付宝、高通……
5、总结
依赖倒置原则(Dependency Inversion Principle,DIP):
a. 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。
b. 抽象不应该依赖于细节;细节应该依赖于抽象。
即:
传统系统:高层模块依赖低层模块。
DIP第一层境界:低层模块依赖高层模块。
DIP第二层境界:无论高层模块或低层模块实现的细节,都依赖于独立出来的抽象层。
依赖倒置原则应用广泛,在面向对象程序框架设计中(是核心原则)、架构系统中、在社会活动构建组织等方面,都发挥重要作用。
DIP在面向对象程序设计中,面向接口编程是基础,而核心是"依赖倒置"。
一般来说,系统中存在违反DIP的地方,很可能就是我们需要优化的地方。
但实际中需要考虑应用DIP的代价。
到这里,我们对依赖倒置原则有了一个大体的了解,后面我们将了解其他的面向对象设计原则......
【参考资料】
1、《敏捷软件开发:原则、模式与实践》第11章 依赖倒置原则(DIP)
2、维基百科"Dependency Inversion Principle"
3、《代码大全》第二版 第5章 软件构件中的设计
4、《Java与模式》第8章 依赖倒转原则(DIP)
5、《大话设计模式》第5章 会修电脑不会修收音机?—依赖倒转原则
6、《Design Patterns》GoF
7、《面向对象分析与设计》第3版