重拾依赖倒置原则(训练营第二课)
DIP - 依赖倒置原则
High level modules should not depend upon low level modules, Both should depend upon abstractions. Abstractions should not depend upon details. Details should depend upon abstracts.
官方翻译:
高层模块不应该依赖低层模块,两者都应该依赖其抽象;
抽象不应该依赖细节
细节应该依赖抽象
这样还是有点抽象,其实它的核心思想就是:要面向接口编程,不要面向实现编程。
用通俗一点的话来说:接口应该属于它的用户,而不是它的实现类(派生类)。
拿常见的 Spring MVC 的 Controller / Service / DAO(Repository) 三层设计来说,Controller 层通常需要调用 Service 的接口,属于 Service 接口的用户。所以 Service 接口的设计应该面向 Controller 的需求来编程,输入参数和输出参数也应该是考虑 Controller 的使用情况来定;而不能就着 Service 的实现方便来设计。
好莱坞原则
“不要给我们打电话,我们会给你打电话(don't call us, we'll call you)”
这是著名的好莱坞原则。大致的解释就是:
在好莱坞,演员把简历递交给演艺公司后就只有回家等待。由演艺公司对整个娱乐项的完全控制,演员只能被动式的接受公司的差使,在需要的环节中,完成自己的演出。
为什么有时候依赖倒置原则又被称为好莱坞原则?
好莱坞原则很直观地说明一个观点:高层模块(演艺公司)不依赖于低层模块(演员),而底层模块不需要也不应该知道高层模块(什么时候调用它)。
当高层模块(演艺公司)需要低层模块(演员)功能的时候,只要通过 抽象的接口(类比:电话)直接调用就可以了。
类比好莱坞原则,当演艺公司需要某个演员的时候,直接通过 电话(接口),就可以让 对应的演员去执行指定的角色演出。演艺公司只需要知道 符合需求的演员电话(符合功能需求的接口)即可;至于演员如何表演,演艺公司就不需要去关心细节了。
回到依赖倒置原则,高层模块只需要知道自己需要依赖的功能接口即可;低层模块具体如何实现该接口的,高层模块不需要也不应该去关心。
DIP 倒置了什么
从上面的原则定义来看,最直观的就是倒置了:
模块或包的依赖关系
但是深入来看,其实这个原则不仅仅适用于模块或包这个层面,从更深入或者更广泛的角度来看,某种程度上也是适合的。具体来说如下:
开发的顺序和职责;如前后端接口设计,通常应该是由前端用户来设计
类 Spring MVC 的多层依赖;例如:
Controller 依赖 Service 接口,且两者应该处于同一层
Service 依赖 DAO 接口,且两者应该处于同一层
DAO 依赖 数据库接口,假如数据库接口有抽象的话,则也应该和 DAO 处于同一层
框架设计和应用代码
框架 不应该依赖于 应用代码
框架 应该定义规范,并提供符合规范的接口。
应用代码 实现框架的接口
框架自动调用实现了符合规范接口的应用代码
DIP 的用例
Sample 1 - Controller & Service
Controller 是高层,不能依赖于 Service 层;但是 Controller 又需要用到 Service 接口。
正解:controller 实现 依赖于 service 抽象
Service 接口必须由 controller 层来设计,controller 依赖于该接口
接口不需要考虑底层的实现细节,比如 DAO 的数据等
接口应该是返回 DTO,而不是底层的 Entity(DAO层);因为 controller 层不知道 底层的 DAO & Entity
Service 接口在定义时和底层无关,所以是属于 controller 层,属于 controller 实现 依赖于 抽象
错解:
假如 Service 接口由 Service 实现层来定义,则接口属于 service 实现层
controller 就会通过依赖 service 接口间接地依赖 service 实现
从而违背 DIP 原则 - 高层依赖低层
Sample 2 - 高层定义接口,底层实现接口
接口属于它的用户,而不是它的实现类(派生类)。例如:接口命名要根据用户的功能需求来命名
service 接口的命名由 controller 根据功能来定义,而不是由 service 层来定义
原则上来说,通常应该是前端定义接口,而后端实现提供接口
Sample 3 - 框架 vs 工具 (e.g. JUnit vs Slf4j)
框架 不能依赖于 应用代码,框架 自动调用 应用代码 (e.g. JUnit)
框架 不是提供接口 给应用代码使用;
框架 是定义规范接口,让应用代码来实现规范的接口
框架 自动调用 符合规范 的应用代码
应用代码不应该调用框架的规范接口,而是去实现框架的接口
应用代码 不应该 调用框架的接口
应用代码 应该 按照规范 实现框架接口 来实现业务功能
框架会 自动地通过 规范接口 来调用 应用代码
JUnit 框架
JUnit 是框架,而不是工具。
应用程序不需要直接调用 JUnit 的接口,而是实现 JUnit 的规范接口(@Test)
JUnit 会自动调用 应用代码
Slf4j 工具
Slf4j 是工具,应用程序直接调用 log 接口
JUnit 如何实现依赖倒置
JUnit 是一个Java语言的单元测试框架。它定义了测试用例的编写规范,并通过驱动符合规范的测试用例的执行来完成框架的功能。
从依赖倒置原则来看,JUnit 就是高层模块,而测试用例就是底层模块。JUnit 不依赖于 测试用例,而测试用例 在某种程度上也不直接依赖 JUnit,两者都是通过依赖于 指定规范的接口,如 @Test, @BeforeTest, @AfterTest 等等。
只要测试用例的编写,按照 @Test 等注解的要求编程;JUnit 会自动扫描并找出所有测试用例,然后执行测试用例。JUnit 并不关心测试用例测试的内容,测试用例也不需要知道 JUnit 如何执行测试用例。
进一步来说,假如有另外一个单元测试框架(如 TestNG)符合相同的规范,那么它也可以用来执行所有的测试用例。所以,底层模块并不直接依赖于高层模块,从而实现了可复用和解耦。
JUnit 和 测试用例 通过依赖于抽象接口 @Test, @BeforeTest,@AfterTest 等注解,实现了依赖倒置原则。
附录
1. 用接口隔离原则优化 Cache 类的设计,画出优化后的类图
版权声明: 本文为 InfoQ 作者【看山是山】的原创文章。
原文链接:【http://xie.infoq.cn/article/a92c88504b8989b04e0065135】。文章转载请联系作者。
评论 (1 条评论)