返回设计模式博客目录
|
第一篇:单一职责原则
第二篇:开闭原则
第三篇:里氏替换原则
第四篇:依赖倒置原则
第五篇:接口隔离原则
第六篇:迪米特原则
依赖倒置原则
英文全称是 Dependence Inversion Principle,缩写 DIP。它指代了一种特定的解耦形式,使得高层次的模块不依赖于低层次模块的实现细节,即依赖模块被颠倒了。它包含了以下几个含义:
- 高层模块不应该依赖低层模块,两者都应该依赖其抽象;
- 抽象不应该依赖细节;
- 细节应该依赖抽象。
在 Java 语言中,抽象就是指接口或抽象类,两者都是不能直接被实例化的;细节就是实现类,实现接口或继承抽象类而产生的类就是细节,可以被直接实例化 (new)。高层模块就是调用端,底层模块就是具体实现类。依赖倒置原则在 Java 语言中的表现就是:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的。概括的说就是面向接口编程,或者说面向抽象编程,这里的抽象指的是接口或者抽象类。
如果类与类直接依赖于细节,那么它们直接就有直接的耦合,当具体实现变化时,意味着要同时修改依赖者的代码,这限制了系统的可扩展性。
在下面的代码中,ImageLoader 直接依赖于 MemoryCache,这个 MemoryCache 是一个具体实现,而不是一个抽象类或者接口。这导致了 ImageLoader 直接依赖了具体细节,当 MemoryCache 不能满足 ImageLoader 而需要被其他缓存实现替换时,此时就必须修改 ImageLoader 的代码。
随着产品的升级,用户发现 MemoryCache 已经不能满足需求,用户需要的 ImageLoader 可以将图片同时缓存到内存和 SD 卡中,或者可以让用户自定义实现缓存。修改原有代码也不符合开闭原则。
正确的做法是依照依赖倒置原则依赖抽象,而不依赖具体实现。如下所示:
在这里,我们建立了 ImageCache 抽象,并且让 ImageLoader 依赖于抽象而不是具体细节。当需求发生变化时,我们只需要实现 ImageCache 类或者继承其他已有的 ImageCache 子类完成相应的缓存功能,然后将具体的实现注入到 ImageLoader 即可实现缓存功能的替换,这就保证了缓存系统的可扩展性,有了拥抱变化的能力,这就是依赖倒置原则。
举例:涛哥开奔驰
先不考虑依赖倒置原则,看一下如下的设计:
从上面的类图中可以看出,司机类和奔驰车类都属于细节,并没有实现或继承抽象,它们是对象级别的耦合。通过类图可以看出司机有一个 drive() 方法,用来开车,奔驰车有一个 run() 方法,用来表示车辆运行,并且奔驰车类依赖于司机类,用户模块表示高层模块,负责调用司机类和奔驰车类。
可用以下代码表示:
这样的设计乍一看好像也没有问题,涛哥只管开着他的奔驰车就好。但是假如有一天他不想开奔驰了,想换一辆宝马车玩玩怎么办呢?我们当然可以新建一个宝马车类,也给它弄一个 run() 方法,但问题是,这辆车有是有了,但是涛哥却不能开啊,因为司机类里面并没有宝马车的依赖。要想解决问题,只能修改代码。
上面的设计没有使用依赖倒置原则,我们已经发现,模块与模块之间耦合度太高,生产力太低,只要需求一变就需要大面积重构,说明这样的设计是不合理。现在我们引入依赖倒置原则,重新设计的类图如下:
可用以下代码表示:
如此设计,涛哥再也不怕有新车不能开的情况了。
依赖的三种方法
接口声明依赖对象: 在接口的方法中声明依赖对象,就如上面的例子。
构造函数传递依赖对象:在类中通过构造函数声明依赖对象,按照依赖注入的说法,这种方式叫做构造函数注入。
Setter 方法传递依赖对象:在抽象中设置 Setter 方法声明依赖对象。
深入理解
依赖倒置原则的本质就是通过抽象(抽象类或接口)使各个类或模块实现彼此独立,不互相影响,实现模块间的松耦合。在项目中使用这个规则需要以下原则:
- 每个类尽量都要有接口或抽象类,或者抽象类和接口都有。
- 变量的表面类型尽量是接口或者抽象类。
- 任何类都不应该从具体类派生。
- 尽量不要重写基类已经写好的方法(里式替换原则)。
如果基类是一个抽象类,而这个方法已经实现了,子类尽量不要覆写。类间依赖的是抽象,覆写了抽象方法,对依赖的稳定性会有一定的影响。 - 结合里式替换原则来使用: 结合里式替换原则和依赖倒置原则我们可以得出一个通俗的规则,接口负责定义 public 属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。
总结:依赖倒置原则的核心就是面向抽象(抽象类或者接口)编程。