对于依赖倒置,常用Spring框架的开发人员可能非常熟悉,这是在Spring 的应用程序经常使用的一种设计原则,他有以下要求:

  • 高层模块不应该依赖低层模块,两者都应该依赖其抽象 假设AImpl 不应该依赖于 BImpl,而是 A 依赖于 B
  • 抽象不应该依赖细节,而是细节依赖抽象

    在Java语言中,细节指的就是实现类,具体的实现细节;而抽象指的就是接口或者抽象类,这两者是不能被实例化的

在Java语言中实现上面的细节就是,面向接口开发 (OOD,Object-Orented Design),具有以下特性:

  • 模块之间通过抽象相互依赖,实现类之间不发生直接的依赖
  • 接口或者抽象类不依赖于实现类
  • 实现类依赖于接口或者抽象类

3.1 依赖倒置的案例

采用DIP 原则主要有以下优点:

  • 采用DIP 可以减少代码间的耦合,降低并行开发的风险
  • 提高代码 的可维护性以及可读性

3.1.1 初步设计方案

创建汽车驾驶员 Driver类型,他可以驾驶BenZ类型的汽车,我们初步的设计可能是下面的UML设计图,

3、依赖倒置原则(DIP) - 图1

  1. public class DIP {
  2. public static void main(String[] args) {
  3. // tom 驾驶 BenZ 汽车
  4. Driver tom = new Driver();
  5. tom.driver(new BenZ());
  6. }
  7. static class Driver {
  8. public void driver(BenZ benZ) {
  9. benZ.run(100);
  10. }
  11. }
  12. static class BenZ {
  13. public void run(int speed) {
  14. System.out.println("奔驰车正在以" + speed + " 速度运行");
  15. }
  16. }
  17. }
  18. /*
  19. 奔驰车正在以100 速度运行
  20. */

思考: 🤔 基于上面的代码,驾驶员 tom 后面有可能会驾驶BWM,QQ 等汽车,怎么办?

那么,根据需要求,我可以创建BWM 类如下:

  1. static class BWM {
  2. public void run(int speed) {
  3. System.out.println("BWM正在以" + speed + " 速度运行");
  4. }
  5. }

但是显然 Driver中的drive方法的参数只能是BenZ类 public void driver(BenZ benZ) 只能驾驶BenZ类的汽车,不能驾驶BWM 类,这显然不合理:

  • Driver类依赖于BenZ这个实现类
  • 需要同时维护多个汽车类,维护成本大大提高
  • 在Driver创建的时候,必须等到BenZ实现才能继续开发,这不能进行单元测试,甚至不能运行,提高了并行开发的风险

3.1.2 改进方案

根据依赖注入的要求我们可以知道,各个类之间不能依赖于细节,而应该依赖于其抽象,基于此,我们将汽车驾驶员和汽车进行抽象,上层仅仅依赖于抽象,而非细节,实现UML如下:

3、依赖倒置原则(DIP) - 图2

我们将驾驶员和汽车做个抽象:

  1. interface ICar {
  2. /** 汽车的名称 */
  3. String name();
  4. /** 汽车运行 */
  5. void run(int speed);
  6. }
  7. interface IDriver {
  8. /** 驾驶ICar 避免依赖其实现的子类 */
  9. void drive(ICar car, int speed);
  10. }

继续实现其ICar和IDriver的实现细节

  1. static class Driver implements IDriver {
  2. @Override
  3. public void drive(ICar iCar, int speed) {
  4. iCar.run(speed);
  5. }
  6. }
  7. static class BenZ implements ICar {
  8. @Override
  9. public String name() {
  10. return "BenZ";
  11. }
  12. @Override
  13. public void run(int speed) {
  14. System.out.println("奔驰车正在以" + speed + " 速度运行");
  15. }
  16. }
  17. static class BWM implements ICar {
  18. @Override
  19. public String name() {
  20. return "BWM";
  21. }
  22. @Override
  23. public void run(int speed) {
  24. System.out.println("BWM正在以" + speed + " 速度运行");
  25. }
  26. }

在使用的过程中,我们可以不在原来子类实现,而是使用抽象类实现

  1. IDriver tom = new Driver();
  2. // tom 可以驾驶BWM
  3. tom.drive(new BWM(), 100);
  4. // tom 也可以驾驶BenZ
  5. tom.drive(new BenZ(), 90);

3.1.3 DIP 对并行开发的影响

上层代码逻辑修改为依赖其抽象之后,只要制定两个类的接口或者抽象类即可,而项目直接的单元测试也可以独立的运行,TDD (测试驱动开发) 就是对DIP最高级的应用。

3.2 依赖的方式

结合Spring的Bean的依赖注入方式可以知道,依赖注入主要有三种方式:

  • 构造注入,在构造的时候注入依赖
  • Setter方法注入
  • 接口方法中注入(汽车的例子使用的就是此方法)

3.3 最佳实践

需要实现依赖倒置,需要有几个必要的基本条件:

  • 每个类尽量需要有抽象类或者接口,这是实现依赖倒置的基本条件
  • 变量的表面类型(参考JVM的表面类型和实际类型) 应该尽量是接口或者抽象类
  • 任何类都不应该从具体类派生
  • 尽量不要复写抽象类的方法,类之间依赖的是抽象,修改了抽象对整个代码的稳定性产生影响
  • 结合LSP(里式替换原则)