依赖倒置原则,接口隔离原则

三、依赖倒置原则

定义

依赖倒置原则(Dependence Inversion Principle ,DIP):高层模块不应该依赖低层模块,应该依赖其抽象,抽象不应该依赖其细节,细节应该依赖其抽象。

理解:低层模块:具体细化的 Java 类。高层模块:是由多个低层模块组成的。抽象:指的是接口或者抽象类。依赖:存在类 A 的一个方法 S, S 传入的参数是另一个类 B 的实例,那么类 A 依赖于类 B, 也就是类 A 中引用了类 B, 则 A 依赖 B, 因为 A 类中缺少了 B 类就无法正常运行了!

实例

先举一个反例子:一个司机开宝马车。正常思维是定义一个司机类 Driver, 并实现一个开车 void drive(BMWCar bmwcar) 的方法,该方法传入的是宝马车的一个实例!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//宝马车类,实现run()方法
public class BMWCar {
public void run(){
System.out.println("宝马车开动了....");
}
}
//司机类,实现开车方法,传入宝马车实例参数
public class Driver {
public void drive(BMWCar bmwcar){
System.out.println("司机开车...");
bmwcar.run();
}
}
//测试
public class Main {
public static void main(String[] args) {
Driver driver=new Driver();
driver.drive(new BMWCar());
}
}

运行结果

1
2
司机开车...
宝马车开动了....

结果得到没有问题!但现在我们更改需求了,司机现在改开奔驰了,那么如果在这个类的基础上更改,我们需要给司机提供一个 drive(BenCar bencar) 的方法。而如果后续要司机开各种车难道都要实现一个方法吗?这不免造成依赖性太强和冗余过度情况!

现在的解决方案:定义接口 ICar, 让 BMWCar 等其他车类都实现 ICar 接口,而司机 Driver 类只需在 drive(ICar car) 的方法里传入 ICar 类型即可实现司机开各种车型的功能!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface ICar {
public void run();
}
public class BMWCar implements ICar{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("宝马开动了。。。");
}
}
public class BenCar implements ICar{
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("奔驰开动了。。。");
}
}

司机Driver类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Driver{
public void drive(ICar car) {
System.out.println("司机开车。。。");
car.run();
}
}
//测试类
public class DipMain {
public static void main(String[] args) {
// TODO Auto-generated method stub
Driver driver=new Driver();
BMWCar bmwcar=new BMWCar();
BenCar bencar=new BenCar();
driver.drive(bmwcar);
driver.drive(bencar);//实现司机开大奔了!
}
}

上面就遵循了依赖倒置原则,另外上面依赖的传递是通过参数直接传的!实际上依赖传递分为三种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//1、通过构造方法传递
class Driver{
private ICar car;
public Driver2(ICar car){
this.car = car;
}
public void drive() {
System.out.println("司机发动车了...");
car.run();
}
}
//2、通过setter方法传递
class Driver{
private ICar car;
public void drive() {
System.out.println("司机开车.....");
car.run();
}
public void setCar(ICar car) {
this.car = car;
}
}
//3、通过接口传递(也就是我们上面例子的使用方法)
class Driver{
public void drive(ICar car) {
System.out.println("司机发动车了...");
car.run();
}
}

结论

在实际编程中,我们一般需要做到如下3点:

1、低层模块尽量都要有抽象类或接口,或者两者都有
2、变量的声明类型尽量是抽象类或接口
3、使用继承时遵循里氏替换原则

依赖倒置原则的核心就是要我们面向接口编程,理解了面向接口编程,也就理解了依赖倒置。

四、接口隔离原则

定义

接口隔离原则 ( Interface Segregation Principle ): 客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上

理解:将臃肿的接口根据其他类需要使用的方法拆分为独立的几个接口。也就是采用接口隔离原则。实际上该原则与单一职责原则有点相似,都是将方法拆分成多份!不同点是单一职责通常适用于类,并且根据职责拆分;而接口隔离原则主要适用于接口,根据其他类的使用需求拆分!

实例

首先定义了一个接口 I, 并定义 5 个方法, 并定义类 A 类 C, 这两个类依赖 I;类A依赖于接口I中的 method1(), method2(), method3()三个方法;类 C 依赖接口I中的 method1(), method()4, method5() 三个方法。

类 B, 类 D 则是 A, B 依赖接口I的具体方法实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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 C{
public void depend1(I i){
i.method1();
}
public void depend2(I i){
i.method4();
}
public void depend3(I i){
i.method5();
}
}
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 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 拆分为三个接口 I1, I2, I3; 而类 B, 类 D 的实现则不用完全实现 I 了,而只需实现自己需要的方法接口了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
interface I1 {
public void method1();
}
interface I2 {
public void method2();
public void method3();
}
interface I3 {
public void method4();
public void method5();
}
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 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");
}
}
//同时A,C中的方法参数类型改为对应的接口即可!
class A{
public void depend1(I1 i){
i.method1();
}
public void depend2(I2 i){
i.method2();
}
public void depend3(I2 i){
i.method3();
}
}
class C{
public void depend1(I1 i){
i.method1();
}
public void depend2(I3 i){
i.method4();
}
public void depend3(I3 i){
i.method5();
}
}

本文例子中,将一个庞大的接口变更为3个专用的接口所采用的就是接口隔离原则。在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。

结论

接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。

采用接口隔离原则对接口进行约束时,要注意以下几点:

1、接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
2、为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
3、提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。

苟且一下