重拾依赖倒置原则(训练营第二课)

用户头像
看山是山
关注
发布于: 2020 年 06 月 16 日

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 类的设计,画出优化后的类图



发布于: 2020 年 06 月 16 日 阅读数: 67
用户头像

看山是山

关注

还未添加个人签名 2018.11.16 加入

还未添加个人简介

评论 (1 条评论)

发布
用户头像
增加封面图就更好了~
2020 年 06 月 17 日 10:03
回复
没有更多了
重拾依赖倒置原则(训练营第二课)