3.依赖倒置原则:High levelmodules should not depend upon low level modules. Both should depend uponabstractions. Abstractions should not depend upon details. Details shoulddepend upon abstractions.
意思是:高层模块不应该依赖底层模块,两者都应该依赖抽象;抽象不应该依赖细节;细节应该依赖抽象;在java语言中,依赖倒置原则表现为:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的;接口或抽象类不依赖于实现类;实现类依赖接口或抽象类;更精简的定义就是“面向接口编程”---OOD(Object-Oriented Design,面向对象设计)的精髓之一。
依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。在java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
依赖倒置原则的中心思想是面向接口编程,我们一个例子来说明面向接口编程比相对于面向实现编程好在什么地方。场景是这样的,驾驶员驾驶汽车,现在给他一辆奔驰车,他就可以驾车拉风了,类图如下:
图10.
实现代码如下:
public class Benz {
//汽车肯定会跑
public void run(){
System.out.println("奔驰汽车开始运行...");
}
}
public class Driver {
//司机的主要职责就是驾驶汽车
public void drive(Benz benz){
benz.run();
}
}
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("宝马汽车开始运行...");
}
}
宝马车也产生了,但是我们却没有办法让张三开动起来,为什么?张三没有开动宝马车的方法呀!一个拿有C照的司机竟然只能开奔驰车而不能开宝马车,这也太不合理了!在现实世界都不允许存在这种情况,何况程序还是对现实世界的抽象,我们的设计出现了问题:司机类和奔驰车类之间是一个紧耦合的关系,其导致的结果就是系统的可维护性大大降低,可读性降低,两个相似的类需要阅读两个文件,你乐意吗?还有稳定性,什么是稳定性?固化的、健壮的才是稳定的,这里只是增加了一个车类就需要修改司机类,这不是稳定性,这是易变性。被依赖者的变更竟然让依赖者来承担修改的成本,这样的依赖关系谁肯承担!
如果不使用依赖倒置原则就会加重类间的耦合性,降低系统的稳定性,增加并行开发引起的风险,降低代码的可读性和维护性。承接上面的例子,引入依赖倒置原则后的类图如图11所示。
图11.
< http://www.2cto.com/kf/ware/vc/" target="_blank" class="keylink">vcD4KPHA+yrXP1rT6wuvI58/Co7o8L3A+CjxwPjxwcmUgY2xhc3M9"brush:java;">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("宝马汽车开始运行..."); } } public class Client { public static void main(String[] args) { IDriver zhangSan = new Driver(); ICar benz = new Benz(); //张三开奔驰车 zhangSan.drive(benz); } }
在新增加低层模块时,只修改了业务场景类,也就是高层模块,对其他低层模块如Driver类不需要做任何修改,业务就可以运行,把“变更”引起的风险扩散降低到最小。
注意 在Java中,只要定义变量就必然要有类型,一个变量可以有两个类型:显示类型和真实类型,显示类型是在定义的时候赋予的类型,真实类型是对象的类型,如zhangSan的显示类型是IDriver,真实类型是Driver。
抽象是对实现的约束,对依赖者而言,也是一种契约,不仅仅约束自己,还同时约束自己与外部的关系,其目的是保证所有的细节不逃脱契约的范畴,确保约束双方按照既定的契约(抽象)共同发展,只要抽象这根基线在,细节就逃脱不了这个圈圈,始终让你的对象做到“言必信,行必果“。依赖的三种写法
依赖是可以传递的,A对象依赖B对象,B又依赖C,C又依赖D…,生生不息,依赖不止,记住一点:只要做到抽象依赖,即使是多层的依赖传递也无所畏惧!
对象的依赖关系有三种方式来传递,如下所示。
- 构造函数传递依赖对象
在类中通过构造函数声明依赖对象,按照依赖注入的说法这种方式叫做构造函数注入,按照这种方式的注入,IDriver和Driver的程序修改后如代码下:
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依赖注入,按照这种方式的注入,IDriver和Driver的程序修改后如下:
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(); } }
- 接口声明依赖对象
在接口的方法中声明依赖对象,该方法也叫做接口注入。
最佳实践:依赖倒转原则的本质就是通过抽象(接口或抽象类)使各个类或模块的实现彼此独立,不互相影响,实现模块间的松耦合,我们怎么在项目中使用这个规则呢?只要遵循以下的几个规则就可以:
- 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备。
这是依赖倒置的基本要求,接口和抽象类都是属于抽象的,有了抽象才可能依赖倒置。
- 变量的显示类型尽量是接口或者是抽象类。
很多书上说变量的类型一定要是接口或者是抽象类,这个有点绝对化了,比如一个工具类,xxxUtils一般是不需要接口或是抽象类的。还有,如果你要使用类的clone方法,就必须使用实现类,这个是JDK提供一个规范。
- 任何类都不应该从具体类派生。
如果一个项目处于开发状态,确实不应该有从具体类派生出的子类的情况,但这也不是绝对的,因为人都是会犯错误的,有时设计缺陷是在所难免的,因此只要不超过两层的继承都是可以忍受的。特别是做项目维护的同志,基本上可以不考虑这个规则,为什么?维护工作基本上都是做扩展开发,修复行为,通过一个继承关系,覆写一个方法就可以修正一个很大的Bug,何必再要去继承最高的基类呢?
- 尽量不要覆写基类的方法。
如果基类是一个抽象类,而且这个方法已经实现了,子类尽量不要覆写。类间依赖的是抽象,覆写了抽象方法,对依赖的稳定性会产生一定的影响。
- 结合里氏替换原则使用
在讲里氏替换原则时,父类出现的地方子类就能出现,再结合本章节的讲解,我们可以得出这样一个通俗的规则: 接口负责定义public属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化。
依赖倒置原则是六个设计原则中最难以实现的原则,它是实现开闭原则的重要途径,依赖倒置原则没有实现,就别想实现对扩展开放,对修改关闭。在项目中,大家只要记住是“面向接口编程”就基本上抓住了依赖倒转原则的核心。
4.接口隔离原则:客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。要求我们在编写代码过程中要建立单一接口,不要建立臃肿庞大的接口,接口尽量细化,同时接口中的方法尽量少!接口隔离原则与单一职责原则的审视角度是不同的。单一职责要求的是类和接口职责单一,注重的是职责,这是业务逻辑上的划分,而接口隔离原则要求接口的方法尽量少。例如一个接口的职责可能包含10个方法,这10个方法都放在一个接口中,并且提供给多个模块访问,各个模块按照规定的权限来访问,在系统外通过文档约束“不使用的方法不要访问”,按照单一职责原则是允许的,按照接口隔离原则是不允许的,因为它要求“尽量使用多个专门的接口”,专门的接口指什么?就是指提供给每个模块都应该是单一接口,提供给几个模块就应该有几个接口,而不是建立一个庞大的臃肿的接口,容纳所有的客户端访问。
下面我们来看一个例子:类A依赖接口I中的方法1、方法2、方法3,类B是对类A依赖的实现。类C依赖接口I中的方法1、方法4、方法5,类D是对类C依赖的实现。对于类B和类D来说,虽然他们都存在着用不到的方法,但由于实现了接口I,所以也必须要实现这些用不到的方法,代码入如下:
interface I { public void method1(); public void method2(); public void method3(); public void method4(); public void method5(); } class A{ public void depend1(I i){ i.method1(); } public void depend2(I i){ i.method2(); } public void depend3(I i){ i.method3(); } } class B implements I{ public void method1() { System.out.println("类B实现接口I的方法1"); } public void method2() { System.out.println("类B实现接口I的方法2"); } public void method3() { System.out.println("类B实现接口I的方法3"); } //对于类B来说,method4和method5不是必需的,但是由于接口A中有这两个方法, //所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。 public void method4() {} public void method5() {} } class C{ public void depend1(I i){ i.method1(); } public void depend2(I i){ i.method4(); } public void depend3(I i){ i.method5(); } } class D implements I{ public void method1() { System.out.println("类D实现接口I的方法1"); } //对于类D来说,method2和method3不是必需的,但是由于接口A中有这两个方法, //所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。 public void method2() {} public void method3() {} public void method4() { System.out.println("类D实现接口I的方法4"); } public void method5() { System.out.println("类D实现接口I的方法5"); } } public class Client{ public static void main(String[] args){ A a = new A(); a.depend1(new B()); a.depend2(new B()); a.depend3(new B()); C c = new C(); c.depend1(new D()); c.depend2(new D()); c.depend3(new D()); } }
可以看到,如果接口过于臃肿,只要接口中出现的方法,不管对依赖于它的类有没有用处,实现类中都必须去实现这些方法,这显然不是好的设计。如果将这个设计修改为符合接口隔离原则,就必须对接口I进行拆分。在这里我们将原有的接口I拆分为三个接口,代码如下:
interface I1 { public void method1(); } interface I2 { public void method2(); public void method3(); } interface I3 { public void method4(); public void method5(); } class A{ public void depend1(I1 i){ i.method1(); } public void depend2(I2 i){ i.method2(); } public void depend3(I2 i){ i.method3(); } } class B implements I1, I2{ public void method1() { System.out.println("类B实现接口I1的方法1"); } public void method2() { System.out.println("类B实现接口I2的方法2"); } public void method3() { System.out.println("类B实现接口I2的方法3"); } } class C{ public void depend1(I1 i){ i.method1(); } public void depend2(I3 i){ i.method4(); } public void depend3(I3 i){ i.method5(); } } class D implements I1, I3{ public void method1() { System.out.println("类D实现接口I1的方法1"); } public void method4() { System.out.println("类D实现接口I3的方法4"); } public void method5() { System.out.println("类D实现接口I3的方法5"); } }
接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。本文例子中,将一个庞大的接口变更为3个专用的接口所采用的就是接口隔离原则。在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
说到这里,很多人会觉的接口隔离原则跟之前的单一职责原则很相似,其实不然。其一,单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口接口,主要针对抽象,针对程序整体框架的构建。
采用接口隔离原则对接口进行约束时,要注意以下几点:
-
接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。
- 结合里氏替换原则使用
- 尽量不要覆写基类的方法。
- 任何类都不应该从具体类派生。
- 变量的显示类型尽量是接口或者是抽象类。
- 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备。
- 接口声明依赖对象
- Setter方法传递依赖对象