查看“依赖倒置原则”的源代码
←
依赖倒置原则
跳到导航
跳到搜索
因为以下原因,您没有权限编辑本页:
您所请求的操作仅限于该用户组的用户使用:
用户
您可以查看和复制此页面的源代码。
=== 依赖倒置原则的定义 === Dependence Inversion Principle 包含三层含义: * 高层模块不应该依赖低层模块,两者都应该依赖其抽象; * 抽象不应该依赖细节; * 细节应该依赖抽象。 高层模块和底层模块容易理解,每一个逻辑的实现都是由原子逻辑组成的,不可分割的原子逻辑就是低层模块,原子逻辑的再组装就是高层模块。 那什么是抽象?什么又是细节呢? 在 Java 语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的; 细节就是实现类,实现接口或继承抽象类而产生的类就是细节, 其特点就是可以直接被实例化,也就是可以加上一个关键字 new 产生一个对象。 依赖倒置原则在 Java 语言中的表现就是: * 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的; * 接口或抽象类不依赖于实现类; * 实现类依赖接口或抽象类。 更加精简的定义就是“面向接口编程”——OOD(Object-Oriented Design,面向对象设计)的精髓之一。 === 言而无信,你太需要契约 === 采用依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。 证明一个定理是否正确,有两种常用的方法: 一种是根据提出的论题,经过一番论证,推出和定理相同的结论,这是顺推证法; 还有一种是首先假设提出的命题是伪命题,然后推导出一个荒谬、与已知条件互斥的结论,这是反证法。 我们今天就用反证法来证明依赖倒置原则是多么优秀和伟大! 论题:依赖倒置原则可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。 反论题:不使用依赖倒置原则也可以减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。 我们通过一个例子来说明反论题是不成立的。 现在的汽车越来越便宜了,一个卫生间的造价就可以买到一辆不错的汽车,有汽车就必然有人来驾驶,死机驾驶奔驰车的类图如图所示: [[文件:司机驾驶奔驰车类图.png|无|缩略图|700x700像素]]奔驰车可以提供一个方法 run ,代表车辆运行,实现过程如代码:<syntaxhighlight lang="java"> /** * 司机源代码 */ public class Driver { public void drive(Benz benz) { benz.run(); } } </syntaxhighlight>司机通过调用奔驰车的 run 方法开动奔驰车,其源代码如:<syntaxhighlight lang="java"> /** * 奔驰车源代码 */ public class Benz { public void run() { System.out.println("奔驰汽车开始运行..."); } } </syntaxhighlight>有车,有司机,在 Client 场景类产生相应的对象,其源代码如:<syntaxhighlight lang="java"> /** * 场景类源代码 */ public class Client { public static void main(String[] args) { Driver zhangSan = new Driver(); Benz benz = new Benz(); //张三开奔驰车 zhangSan.drive(benz); } } </syntaxhighlight>通过以上的代码,完成了司机开动奔驰车的场景,到目前为止,这个司机开奔驰车的项目没有任何问题。 我们常说“危难时刻见真情”,我们把这句话移植到技术上就成了“变更才显真功夫”,业务需求变更永无休止,技术前进就永无止境,在发生变更时才能发觉我们的设计或程序是否是松耦合。 我们在一段貌似磐石的程序上加上一块小石头:张三司机不仅要开奔驰车,还要开宝马车,又该怎么实现呢? 麻烦出来了,那好,我们走一步是一步,我们先把宝马车产生出来,实现过程如下:<syntaxhighlight lang="java"> /** * 宝马车源代码 */ public class BMW { public void run() { System.out.println("宝马汽车开始运行..."); } } </syntaxhighlight>宝马车也产生了,但是我们却没有办法让张三开动起来,为什么? 张三没有开动宝马车的方法呀!一个拿有 C 驾照的司机竟然只能开奔驰车而不能开宝马车,这也太不合理了! 在现实世界都不允许存在这种情况,何况程序还是对现实世界的抽象,我们的设计出现了问题:司机类和奔驰车类之间是紧耦合的关系,其导致的结果就是系统的可维护性大大降低,可读性降低,两个相似的类需要阅读两个文件,你乐意吗? 还有稳定性,什么是稳定性?固化的、健壮的才是稳定的,这里只是增加了一个车类就需要修改司机类,这不是稳定性,这是易变性。 被依赖者的变更竟然让依赖者来承担修改的成本,这样的依赖关系谁肯承担!证明到这里,我们已经知道反论题已经部分不成立了。 '''<big>注意</big>''' '''设计是否具备稳定性,只要适当地“松松土”,观察“设计的蓝图”是否还可以茁壮地成长就可以得出结论,稳定性较高的设计,在周围环境频繁变化的时候,依然可以做到“我自岿然不动”。''' 我们继续证明,“减少并行开发引起的风险”,什么是并行开发的风险? 并行开发最大的风险就是风险扩散,本来只是一段程序的错误或异常,逐步波及一个功能,一个模块,甚至到最后毁坏了整个项目。 为什么并行开发就有这样的风险呢?一个团队,20个开发人员,各人负责不同的功能模块,甲负责汽车类的建造,乙负责司机类的建造,在甲没有完成的情况下,乙是不能完全地编写代码的,缺少汽车类,编译器根本就不会让你通过! 在缺少 Benz 类的情况下,Driver 类能编译吗?更不要说是单元测试了!在这种不使用依赖倒置原则的环境中,所有的开发工作都是“单线程”的,甲做完,乙再做,然后是丙继续……这在20世纪90年代“个人英雄主义”编程模式中还是比较适用的,一个人完成所有的代码工作。 但在现在的大中型项目中已经是完全不能胜任了,一个项目是一个团队协作的结果,一个“英雄”再牛也不可能了解所有的业务和所有的技术,要协作就要并行开发,要并行开发就要解决模块之间的项目依赖关系,那然后呢? 依赖倒置原则就隆重出场了! 根据以上证明,如果不使用依赖倒置原则就会加重类间的耦合性,降低系统的稳定性,增加并行开发引起的风险,降低代码的可读性和可维护性。 承接上面的例子,引入依赖倒置原则后的类图如图: [[文件:引入依赖倒置原则后的类图.png|无|缩略图|550x550像素]] 建立两个接口:IDriver 和 ICar,分别定义了司机和汽车的各个职能,司机就是驾驶汽车,必须实现 drive() 方法,其实现过程如代码:<syntaxhighlight lang="java"> /** * 司机接口 */ public interface IDriver { //是司机就应该会驾驶汽车 public void drive(ICar car); } </syntaxhighlight>接口只是一个抽象化的概念,是对一类事物的最抽象描述,具体的实现代码由相应的实现类来完成,Driver 实现类如代码:<syntaxhighlight lang="java"> /** * 司机类的实现 */ public class Driver implements IDriver { //司机的主要职责就是驾驶汽车 @Override public void drive(ICar car) { car.run(); } } </syntaxhighlight>在 IDriver 中,通过传入 ICar 接口实现了抽象之间的依赖关系,Driver 实现类也传入了 ICar 接口,至于到底是哪个型号的 Car,需要在高层模块中声明。 ICar 及其两个实现类的实现过程如代码:<syntaxhighlight lang="java"> /** * 汽车接口 */ public interface ICar { //是汽车就应该能跑 public void run(); } </syntaxhighlight><syntaxhighlight lang="java"> public class Benz implements ICar { //汽车肯定会跑 @Override public void run() { System.out.println("奔驰汽车开始运行..."); } } </syntaxhighlight><syntaxhighlight lang="java"> public class BMW implements ICar { //宝马车当然也可以开动了 @Override public void run() { System.out.println("宝马汽车开始运行..."); } } </syntaxhighlight>在业务场景中,我们贯彻“抽象不应该依赖细节”,也就是我们认为抽象(ICar 接口)不依赖 BMW 和 Benz 两个实现类(细节),因此在高层次的模块中应用都是抽象,Client 的实现过程如代码:<syntaxhighlight lang="java"> /** * 业务场景 */ public class Client { public static void main(String[] args) { IDriver zhangSan = new Driver(); ICar benz = new Benz(); //张三开奔驰车 zhangSan.drive(benz); } } </syntaxhighlight>Client 属于高层业务逻辑,它对低层模块的依赖都建立在抽象上,zhangSan 的表面类型是 IDriver,Benz 的表面类型是 ICar,也许你要问,在这个高层模块中也调用到了低层模块,比如 new Driver() 和 new Benz() 等,如何解释? 确实如此,zhangSan 的表面类型是 IDriver ,是一个接口,是抽象的、非实体化的,在其后的所有操作中,zhangSan 都是以 IDriver 类型进行操作,屏蔽了细节对抽象的影响。 当然,张三如果要开宝马车,也很容易,我们只要修改业务场景类就可以,实现过程如代码:<syntaxhighlight lang="java"> /** * 张三驾驶宝马车的实现过程 */ public class Client { public static void main(String[] args) { IDriver zhangSan = new Driver(); ICar bmw = new BMW(); //张三开奔驰车 zhangSan.drive(bmw); } } </syntaxhighlight>在新增加低层模块时,只修改了业务场景类,也就是高层模块,对其他低层模块如 Driver 类不需要做任何修改,业务就可以运行,把“变更”引起的风险扩散降到最低。 '''<big>注意</big> 在 Java 中,只要定义变量就必然要有类型,一个变量可以有两种类型:表面类型和实际类型,表面类型是在定义的时候赋予的类型,实际类型是对象的类型,如 zhangSan 的表面类型是 IDriver,实际类型是 Driver。''' 我们再来思考依赖倒置对并行开发的影响。两个类之间有依赖关系,只要制定出两者之间的接口(或抽象类)就可以独立开发了,而且项目之间的单元测试也可以独立地运行,而 TDD(Test-Driven Development,测试驱动开发)开发模式就是依赖倒置原则的最高级应用。 我们继续回顾上面司机驾驶汽车的例子,甲程序员负责 IDriver 的开发,乙程序员负责 ICar 的开发,两个开发人员只要制定好了接口就可以独立地开发了,甲开发进度比较快,完成了 IDriver 以及相关的实现类 Driver 的开发工作,而乙程序员滞后开发,那甲是否可以进行单元测试呢?答案是可以,我们引入一个 JMock 工具,其最基本的功能是根据抽象虚拟一个对象进行测试,测试类如代码:<syntaxhighlight lang="java"> /** * 测试类 */ public class DriverTest extends TestCase { Mockery context = new JUnit4Mockery(); @Test public void testDriver() { //根据接口虚拟一个对象 final ICar car = context.mock(ICar.class); IDriver driver = new Driver(); //内部类 context.checking(new Expectations() {{ oneOf(car).run(); }}); driver.drive(car); } } </syntaxhighlight>注意 “final ICar car = context.mock(ICar.class);”部分,我们只需要一个 ICar 的接口,就可以对 Driver 类进行单元测试。 从这一点来看,两个相互依赖的对象可以分别进行开发,孤立地进行单元测试,进而保证并行开发的效率和质量,TDD 开发的精髓不就在这里吗? 测试驱动开发,先写好单元测试类,然后再写实现类,这对提高代码的质量有非常大的帮助,特别适合研发类项目或在项目成员整体水平比较低的情况下采用。 抽象是对实现的约束,对依赖者而言,也是一种契约,不仅仅约束自己,还同时约束自己与外部的关系,其目的是保证所有的细节不脱离契约的范畴,确保约束双方按照既定的契约(抽象)共同发展,只要抽象这根基线在,细节就脱离不了这个圈圈,始终让你的对象做到“言必信,行必果”。 === 依赖的三种写法 === 依赖是可以传递的,A对象依赖B对象,B又依赖C,C又依赖D……生生不息,依赖不止,记住一点:只要做到抽象依赖,即使是多层的依赖传递也无所畏惧! 对象的依赖关系有三种方式来传递,如下所示: ==== 构造函数传递依赖对象 ====
返回至
依赖倒置原则
。
导航菜单
个人工具
登录
名字空间
页面
讨论
变种
视图
阅读
查看源代码
查看历史
更多
搜索
导航
首页
Spring Boot 2 零基础入门
Spring Cloud
Spring Boot
设计模式之禅
VUE
Vuex
Maven
算法
技能树
Wireshark
IntelliJ IDEA
ElasticSearch
VirtualBox
软考
正则表达式
程序员精讲
软件设计师精讲
初级程序员 历年真题
C
SQL
Java
FFmpeg
Redis
Kafka
MySQL
Spring
Docker
JMeter
Apache
Linux
Windows
Git
ZooKeeper
设计模式
Python
MyBatis
软件
数学
PHP
IntelliJ IDEA
CS基础知识
网络
项目
未分类
MediaWiki
镜像
问题
健身
国债
英语
烹饪
常见术语
MediaWiki帮助
工具
链入页面
相关更改
特殊页面
页面信息