00.5 ANDROID 面向对象的六大原则——接口隔离原则

返回设计模式博客目录
|
第一篇:单一职责原则
第二篇:开闭原则
第三篇:里氏替换原则
第四篇:依赖倒置原则
第五篇:接口隔离原则
第六篇:迪米特原则


接口隔离原则


英文全称是 Interface Segregation Principles,ISP。其定义是:客户端不应该依赖它不需要的接口。另一种定义是:类间的依赖关系应该建立在最小的接口上。接口隔离原则将非常庞大、臃肿的接口拆分成更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。接口隔离原则的目的是系统解开耦合,从而容易重构、更改和重新部署。

说白了就是,让客户端依赖的接口尽可能地小。这样说可能还有点抽象,我们还是以一个示例来说明一下。在此之前我们来说一个场景,在 Java 6 及之前的 JDK 版本,有一个非常讨厌的问题,那就是在使用了 OutputStream 或者其他可关闭的对象之后,我们必须保证他们最终被关闭了,我们的 SD 卡缓存类中就有这样的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void put(String url, Bitmap bitmap) {
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(CACHE_DIR
+ ImageUtil.urlToMd5(url));
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

我们看到的这段代码可读性非常差,各种 try…catch 嵌套都是些简单的代码,但是会严重影响代码的可读性,并且多层次的大括号很容易将代码写到错误的层级中。

在 Java 中有一个 Closeable 接口,该接口标识了一个可关闭的对象,它只有一个 close 方法。如下图所示:

我们要讲的 FileOutputStream 类就实现了这个接口。我们从上图可以看到,还有 100 多个类实现了 Closeable 这个接口。这意味着,在关闭这 100 多个类型的对象时,都需要写出像 put 方法中 finally 代码段那样的代码。这还了得,反正我是不能忍。既然都是实现了 Closeable 接口,那只要一个方法统一来关闭这些对象不就可以了么?于是写来来如下的工具类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.xxt.xtest;
import java.io.Closeable;
public class CloseUtils {
private CloseUtils() {}
public static void closeQuietly(Closeable closeable) {
if (null != closeable) {
try {
closeable.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

我们再看看把这段代码运用到上述的 put 方法中的效果如何。

1
2
3
4
5
6
7
8
9
10
11
12
public void put(String url, Bitmap bitmap) {
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(CACHE_DIR
+ ImageUtil.urlToMd5(url));
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
CloseUtils.closeQuietly(fileOutputStream);
}
}

代码简洁了很多!而且这个 closeQuietly 方法可以运用到各类可关闭的对象中,保证了代码的重用性。CloseUtils 的 closeQuietly 方法的基本原理就是依赖于 Closeable 抽象而不是具体实现,并且建立在最小化依赖原则的基础上,它只需要知道这个对象是可关闭的,其他的一概不关心,也就是这里的接口隔离原则。

试想一下,如果在只是需要关闭一个对象时,它却暴露了其他的接口函数,如 OutputStream 的 write 方法,这就使得更多的细节暴露在客户端代码面前,不仅没有很好地隐藏实现,还增加了接口的使用难度。而通过 Closeable 接口将可关闭的对象抽象起来,这样只需要客户端依赖于 Closeable 就可以对客户端隐藏其他的接口信息,客户端代码只需要知道这个对象可关闭(只可调用 close 方法)即可。

之前博客中设计的 ImageLoader 持有的 ImageCache 就是接口隔离原则的运用。ImageLoader 只需要知道该缓存对象有存、取缓存图片的接口即可,其他的一概不管,这就使得缓存功能的具体实现对 ImageLoader 隐藏。这就是用最小化接口隔离了实现类的细节,也促使我们将庞大的接口拆分到更细粒度的接口当中,这使得我们的系统具有更低的耦合性、更高的灵活性。


举例:拆分接口


依据接口隔离原则,将下图中臃肿的接口 I 拆分为独立的几个接口。

类 A 依赖接口 I 中的方法1、方法2、方法3,类 PA 是对类 A 依赖的实现。类 B 依赖接口 I 中的方法1、方法4、方法5,类 PB 是对类 B 依赖的实现。对于类 PA 和类 PB 来说,虽然他们都存在着用不到的方法(也就是图中划红线的方法),但由于实现了接口 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
72
73
74
75
76
77
78
79
80
interface I {
void method1();
void method2();
void method3();
void method4();
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 {
public void depend1(I i){
i.method1();
}
public void depend2(I i){
i.method4();
}
public void depend3(I i){
i.method5();
}
}
class PA implements I {
public void method1() {
System.out.println("类 PA 实现接口 I 的方法1");
}
public void method2() {
System.out.println("类 PA 实现接口 I 的方法2");
}
public void method3() {
System.out.println("类 PA 实现接口 I 的方法3");
}
// 对于类 PA 来说,method4 和 method5 不是必需的,但是由于接口 I 中有这两个方法
// 所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现
public void method4() {}
public void method5() {}
}
class PB implements I {
public void method1() {
System.out.println("类 PB 实现接口 I 的方法1");
}
// 对于类 PB 来说,method2 和 method3 不是必需的,但是由于接口 I 中有这两个方法
// 所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现
public void method2() {}
public void method3() {}
public void method4() {
System.out.println("类 PB 实现接口 I 的方法4");
}
public void method5() {
System.out.println("类 PB 实现接口 I 的方法5");
}
}
public class Client {
public static void main(String[] args) {
A a = new A();
a.depend1(new PA());
a.depend2(new PA());
a.depend3(new PA());
B b = new B();
b.depend1(new PB());
b.depend2(new PB());
b.depend3(new PB());
}
}

可以看到,如果接口过于臃肿,只要接口中出现的方法,不管对依赖于它的类有没有用处,实现类中都必须去实现这些方法,这显然不是好的设计。如果将这个设计修改为符合接口隔离原则,就必须对接口 I 进行拆分。在这里我们将原有的接口 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
interface I1 {
void method1();
}
interface I2 {
void method2();
void method3();
}
interface I3 {
void method4();
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 {
public void depend1(I1 i){
i.method1();
}
public void depend2(I3 i){
i.method4();
}
public void depend3(I3 i){
i.method5();
}
}
class PA implements I1, I2 {
public void method1() {
System.out.println("类 PA 实现接口 I1 的方法1");
}
public void method2() {
System.out.println("类 PA 实现接口 I2 的方法2");
}
public void method3() {
System.out.println("类 PA 实现接口 I2 的方法3");
}
}
class PB implements I1, I3 {
public void method1() {
System.out.println("类 PB 实现接口 I1 的方法1");
}
public void method4() {
System.out.println("类 PB 实现接口 I2 的方法4");
}
public void method5() {
System.out.println("类 PB 实现接口 I2 的方法5");
}
}

接口隔离原则的含义是:建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口中的方法尽量少。也就是说,我们要为各个类建立专用的接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。本文例子中,将一个庞大的接口变更为 3 个专用的接口所采用的就是接口隔离原则。在程序设计中,依赖几个专用的接口要比依赖一个综合的接口更灵活。接口是设计时对外部设定的“契约”,通过分散定义多个接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。

说到这里,很多人会觉的接口隔离原则跟之前的单一职责原则很相似,其实不然。其一,单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口接口,主要针对抽象,针对程序整体框架的构建。

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

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

运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。