设计模式之设计原则-依赖倒置原则
Posted 阿C_C
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式之设计原则-依赖倒置原则相关的知识,希望对你有一定的参考价值。
依赖倒置原则
依赖倒置原则最原始的定义是这样的:
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 abstractions.
翻译过来就是:
- 高层模块不应该依赖于低层模块,他们都应该依赖于各自的抽象
- 抽象不能依赖于细节,但是细节应该依赖于抽象
我们先来解决出现的两对新名词,之所以成对解释,是因为单个解释没有对比性,理解力不够。
高层模块和低层模块
高层模块,表示的是组合度较高的模块,是和低层模块相对应的概念,这个要看和什么进行对比,打个比方:我们如果把汽车看成一个高层模块,则组成汽车的每个部分,例如中控,发动机,变速箱,地盘甚至轮胎,这些相应的都叫做低层模块,再者,换一个角度,如果我们把轮胎看成一个高层模块,则组成轮胎的每个部件或者是零件,就成了低层模块,这么一说,应该明白了高层模块和低层模块的区别,这个我们平常在软件开发中说的上层和低层是一样的概念。
抽象和细节
抽象简单理解指的是规范,而细节,指的是对该规范的实现,要明确的是,谈论细节或者实现,都是要指明是对该抽象或者规范的实现或者细节,是有一定的继承或者实现关系的,举个例子,我们知道对于系统调用,都会提供有一套系统调用的规范,只要我们传入符合规范的参数和数据,系统就会为我们执行某些要求的行为或者返回计算的结果,那在这个场景下,这个规范就是抽象,而实现这套规范的操作系统,就是细节,反映在Java语言中,接口和抽象类就是抽象,实现接口或继承抽象类而产生的类就是细节,实现类可以直接使用new 关键字进行实例化。
所以,依赖倒置在Java中可以如下进行定义:
- 模块之间通过接口或者抽象类互相调用,实现类之间不发生直接依赖关系
- 接口或者抽象类不依赖于实现类,但实现类要依赖于接口或者抽象类
契约的重要性
依赖倒置原则有如下优点:
- 减少类之间的耦合性,提高系统稳定性
- 降低并行开发引起的风险,提高代码的可读性和可维护性
举个例子,我们有如下类图:
那么对应的类应该是这么写:
司机
public class Driver
//司机的主要职责就是驾驶汽车
public void drive(Benz benz)
benz.run();
奔驰车
public class Benz
//汽车肯定会跑
public void run()
System.out.println("奔驰汽车开始运行...");
调用程序
public class Client
public static void main(String[] args)
Driver zhangSan = new Driver();
Benz benz = new Benz();
//张三开奔驰车
zhangSan.drive(benz);
这一切看起来很完美,但是现在问题来了,老板换车了,换了一台宝马车:
宝马车
public class BMW
//宝马车当然也可以开动了
public void run()
System.out.println("宝马汽车开始运行...");
那这个时候,张三就开不了了,为啥呢?因为我们在定义Driver的时候,就已经指定这个司机只能开Benz,传BMW的对象进去当然不行了,这说明我们的Driver类和Benz类耦合太严重,导致我们新的业务需求无法实现,这就是我们说的系统的可维护性低,再者我们这样设计也会降低稳定性,也就是说,随便的一个新的或者修改的业务需求,我们就需要修改相关的基础类,降低了系统的稳定性。
再者,依赖倒置原则还可以减少并行开发引起的风险,那么并行开发有什么风险呢?并行开发最大的风险是风险扩散,指的是,本来只是一段程序的异常,最终会导致一个功能,模块甚至于整个项目的毁坏,这就是风险扩散。
那么并行开发为什么会导致风险扩散呢?举个例子,一个团队有多个开发人员,每个人员负责项目的不同模块,有的负责Driver,有的负责Benz,有的负责BMW,因为项目的模块有依赖性,如果按照上面的情况设计,负责Benz或者BMW的模块没有完成,则Driver模块不能编译,因为缺少Benz或者BMW,编译器也不会通过,那么我们的开发模式就变成了多人串行开发模式,只有当被依赖的开发人员工作完成之后,依赖这个模块的开发人员才能继续往下进行,这并不是真正意义上的并行开发,要真正实现并行开发,则要解决模块之间的强耦合依赖关系,每个模块都可以不依赖其他模块而且能够独立开发完成,这个时候就要使用到依赖倒置原则。
那怎么样引入依赖倒置原则解决上面的问题呢?设计是这样的:
我们在Driver和两个汽车之上,设计两个接口IDriver以及ICar,Driver实现IDriver接口,driver方法中传入参数为ICar类型,BMW以及Benz类实现ICar接口,代码如下所示:
司机接口
public interface IDriver
//是司机就应该会驾驶汽车
public void drive(ICar car);
司机实现类
public class Driver implements IDriver
//司机的主要职责就是驾驶汽车
public void drive(ICar car)
car.run();
汽车接口及其两个实现类
public interface ICar
//是汽车就应该能跑
public void run();
public class Benz implements ICar
//汽车肯定会跑
public void run()
System.out.println("奔驰汽车开始运行...");
public class BMW implements ICar
//宝马车当然也可以开动了
public void run()
System.out.println("宝马汽车开始运行...");
如此这般,我们实现了抽象不依赖于细节,而依赖于抽象,即ICar接口不依赖于BMW或者Benz,而仅仅依赖于ICar,这也是该项目中的高层次模块,业务场景中是这样的:
public class Client
public static void main(String[] args)
IDriver zhangSan = new Driver();
ICar benz = new Benz();
//张三开奔驰车
zhangSan.drive(benz);
这里司机和汽车使用的都是抽象接口,这里的zhangsan以及benz都是按照接口进行操作的,所以屏蔽了细节对接口的影响,进一步,如果有新的需求,zhangsan想要开BMW车,只需要将ICar benz = new Benz()
更换为ICar bmw = new BMW()
即可,这些都是在业务场景中进行的修改,我们根本不用改动低层次模块Driver,Benz或者是BMW,更不会影响高层模块IDriver或者ICar,这样把风险降到了最小。
我们再来考虑并行开发的影响,之所以会影响并行开发是因为模块依赖,我们知道,模块之间如果有依赖关系,只需要制定出两者之间的接口或者抽象类就可以了,我们继续原来的例子,甲负责IDriver的开发,乙负责ICar开发,只需要提前商定好接口,两者即可独立的进行开发,如果甲开发进度较快,而乙进度滞后,也不会对甲造成什么影响,如果此时甲想要进行单元测试,只需要Mock出一个ICar的实现类对象,即可对IDriver以及实现类Driver进行测试了:
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);
可见,即使两者的开发进度不一样,也可以分别进行测试,这也是测试驱动开发的精髓所在。
抽象,对实现而言,是一个约束,所有实现类必须对抽象进行实现,对其他依赖模块而言,是一种契约,抽象定义的接口,既约束了自己,也约束了自己与外部的关系,保证实现细节不会脱离契约的范畴,为其他依赖模块提供支持,双方按照约定的接口共同发展,只要接口在,细节就不会偏离太远,始终为其他依赖模块提供坚实的依赖细节。
依赖的传递
针对于对象依赖的声明,有三种方式:
通过构造函数传递依赖关系
这种方式通过构造函数声明依赖对象,这种方式叫做构造函数注入,示例如下:
public interface IDriver
//是司机就应该会驾驶汽车
public void drive();
public class Driver implements IDriver
private ICar car;
//构造函数注入
public Driver(ICar _car)
this.car = _car;
//司机的主要职责就是驾驶汽车
public void drive()
this.car.run();
通过Setter方法传递依赖关系
这种方式,通过Setter方法,为外部提供设置依赖对象的方式,这种方法叫做Setter依赖注入,示例如下:
public interface IDriver
//车辆型号
public void setCar(ICar car);//是司机就应该会驾驶汽车
public void drive();
public class Driver implements IDriver
private ICar car;
public void setCar(ICar car)
this.car = car;
//司机的主要职责就是驾驶汽车
public void drive()
this.car.run();
通过接口方法传递依赖关系
我们最开始使用的,就是这种方法,也叫做接口注入。
最佳实践
依赖倒置原则,在项目中使用可以使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合,那我们在项目中怎么应用这个原则呢?有一下几个规则:
尽量有接口或者抽象类
这也是依赖倒置原则的基础,要依赖抽象就必须先有抽象才对。
变量的表面类型,尽量是接口或者抽象类型
并不要求一定是接口或者抽象类型,某些工具类,或者是类的clone方法的使用,就要求是实现类的类型。
实现类尽量不再派生
这只是一个软性规则,在某些场景下,例如设计有缺陷,或者进行项目维护的时候,也是可以从实现类中派生的,根据具体的场景来做措施。
尽量不重写基类的方法
如果基类是抽象类并且已经实现了方法,那么就尽量不要重写该方法,因为该方法有可能会被其他的抽象所依赖,重写该方法,会破坏抽象接口的稳定性。
结合里氏替换原则
里氏替换原则同样可以应用到这里,父类出现的地方子类就可以出现,到我们这里可以理解为基类,接口或者抽象类可以出现的地方,就可以使用实现类对象,由接口来定义公用的属性和方法,抽象类实现公共的构造部分,由实现类实现准确的业务逻辑,并对父类进行细化。
小结
要想彻底理解依赖倒置,我们先来说说依赖正置,依赖正置指的是实现类依赖实现类,这也是我们生活中的思考方式,例如,开车依赖宝马,喝酒依赖二锅头,等等,这些是实现类之间的依赖,而我们编程,需要对现实世界的事物进行抽象,接口和抽象类就是抽象的结果,然后根据我们的系统设计,这些抽象类和接口之间产生了依赖关系,就产生了依赖倒置。
在小型项目中,依赖倒置原则的优点很难体现,但是在大型项目中,依赖倒置原则的优点就会体现出来,特别是用于规避一些非技术原因引起的问题,项目越大,需求变化的概率越大,使用依赖倒置原则对实现类进行约束,可以很好的避免因为需求变化导致工作量剧增,另外,如果在大型项目中人员变动,则使用依赖倒置原则可以避免受到影响,而且维护人员也可以简单轻松的进行维护。
依赖倒置原则是实现开闭原则的基础,只要抓住“面向接口编程”这一核心思想,基本上不会脱离依赖倒置原则太远。另外在项目中,使用依赖倒置原则也要审时度势,每一个设计原则的优点也是有限度的,不要抓住原则不放,而不考虑项目的实际情况,原则是死的,但是设计者是活的,项目的实际状况是动态变化的,项目的最终目标是上线和盈利,技术只是实现目的的工具,切忌耍花枪式的过度设计。
以上是关于设计模式之设计原则-依赖倒置原则的主要内容,如果未能解决你的问题,请参考以下文章