定义
高层模块不应该依赖低层模块,两者都应该依赖抽象。抽象不应该依赖细节,细节应该依赖抽象。
在依赖倒置原则中,“依赖”不局限于UML中的依赖关系(虚线箭头 局部变量),泛指调用或使用关系,譬如 在A类中用到了B类,则可以简单称A依赖B。
“倒置”体现了遵循依赖倒置原则的设计在类图上表现,从词义表面较难理解究竟 什么东西发生了倒置。需要结合场景,分析发生倒置前后的两种类图,对比理解“倒置”的实际含义。假设tyler学习网课,在学网课有java、python、scala,后续还要学习其他网课。按照面向过程的编程思路,设计业务场景的调用关系如下图所示。
上图中,箭头指向了被依赖方,即tyler依赖了三个具体的实现类。这种设计思路因符合人类的一般思考顺序,在实践中可能会被使用,这就为后期系统的维护扩展带来了困难,特别对于多人合作的大型项目的影响尤为突出。由于tyler和具体的课程产生了强依赖关系,两者具有较高的耦合度。对于后期业务需求变化,譬如要学习新课程go语言,则需要修改tyler的历史代码,此时存在引入新问题的可能性,进而需要对新旧代码重新测试,增加了维护成本。
在了解设计模式相关知识后,可按照依赖倒置原则,重新设计系统的整体结构,如下图所示。我们将所有课程抽象为了接口ICourse,tyler不再直接去调用各个具体的课程,而是直接面向接口编程,这就使tyler和具体课程解耦,新增课程也不用修改tyler的历史代码。这就避免了上一版本的问题。
![](https://i-blog.csdnimg.cn/blog_migrate/82253bfff9510e87aca734e781df02c2.png)
上文描述的高层和低层(不是底层)体现在调用或使用关系中,譬如 A 调用B,则可成A为高层,B为低层。这种调用关系也可以理解为依赖关系。
作用
-
降低类之间的耦合性。通过抽象接口作为中介,使高层调用者 和底层组件解耦。
-
提高系统稳定性。将业务模型分为变化和稳定两部分,且对变化部分进行抽象封装,使稳定部分只同抽象接口交互,系统较稳定。
在区分系统中哪部分是变化、哪部分是稳定,需要深入理解业务、搞清楚调用关系;可通过对应关系进行基本确定,比如 甲:乙 = 1:N,则一般来说甲可作为稳定,乙作为变化
-
提高代码可读性和可维护性。抽象作为交互的中介,抽象的具体实现对高层是透明的,可维护性较好。
实现方法
设计模式一直在提倡针对接口,不针对实现的编程方式,依赖倒置原则正是通过这种方式实现的,但依赖倒置更强调的是抽象,使高低层组件都依赖抽象。在编程实践中,如果遵循以下4点,可以满足依赖倒置原则。
- 每个类尽量提供接口或抽象类,或者两者都具备。
- 变量的声明类型尽量是接口或者是抽象类。
- 任何类都不应该从具体类派生。
- 使用继承时尽量遵循里氏替换原则。
代码实践
上图一中面向过程的代码如下,这种方式的问题在于每次需求变化都要修改tyler的代码,稳定性和可扩展性较差。
public class Tyler {
public void studyJavaCourse(){
System.out.println("tyler在学习java课程");
}
public void studyPythonCourse(){
System.out.println("tyler在学习python课程");
}
//当产生新的课程时,直接在调用者中添加新课程代码
public void studyScalaCoutse(){
System.out.println("tyler在学习Scala课程");
}
}
上图二的代码要分为两个部分,稳定部分(tyler)的代码段如下。
public class Tyler {
//只针对接口ICoures编程,不关注具体实现
public void studyImoocCourse(ICourse iCourse){
iCourse.studyCourse();
}
}
变化部分的代码分为抽象类(ICourse)和具体实现类(JavaCourse,PythonCourse),如下代码段。
public interface ICourse {
//接口定义了实现类要完成的动作,是一种锲约和规范
void studyCourse();
}
public class JavaCourse implements ICourse {
//实现接口中的定义
@Override
public void studyCourse() {
System.out.println("tyler在学习java课程");
}
}
public class PythonCourse implements ICourse{
//实现接口中的定义
@Override
public void studyCourse() {
System.out.println("tyler在学习python课程");
}
}
编程知识点
-
在tyler类的代码中,向tyler中传递接口ICourse有三种方式:
- 通过构造器
- 通过方法参数 (本例采用的方法)
- 通过setter方法
这三种方式是学习过程中的笔记,查阅资料发现这属于spring中的依赖注入相关知识,其内容不是本文重点,不再详述。
参考文献
- 依赖倒置原则——面向对象设计原则
- 说说依赖注入
- Head First 设计模式(中文版)第四章 工厂模式