接口做作为面向抽象编程中的一环,有无可替代的地位。那么,什么样的接口设计才算是好的设计,或者说如何设计一个好的接口?
就接口的设计而言,我认为”接口隔离(简称ISP)”原则已经足够.现在我们重新来回忆下:
Clients should not be forced to depend upon interfaces that they don’t use
The dependency of one class to another one should depend on the smallest possible interface.
大意是:客户端不应该强行依赖他不需要的接口.类之间的依赖关键应该建立在最小的接口上.简单的来说,就是接口的设计在满足客户端需求的基础上要尽可能的小(方法少),也就是接口中提供的方法应该都是客户端所需要的.
比如说类A通过接口I依赖类B的test1()和test2()方法,类C通过接口I依赖类D的test3()和test4(),那么此时结构是:
不难发现,此时I对于类A和类B来都分别提供多余的方法,也就是I对两者来说都不是最小接口.而实际上我们所需要的是这样的结构:
也就是我们需要将原有的接口I拆分成类A和类B各自需要的接口I和接口J.现在我们在看看”接口隔离”这个词,不难发现:
接口隔离的目标是使得每个接口尽可能的保持小而美,而接口隔离恰好描述的是实现”小而美”的方法,即通过对接口按照客户端要求进行合理的的拆分.
到这里有写同学迷惑:难道我们先要设计一个不合理的接口,然后对其进行划分么?当然不是,接口隔离的主旨在于设计最小的接口.但在很多情况下,我们无法一开始就设计出最合理的接口,所以在后期会根据实际开发进行调整.
到现在,我们来总结一下接口隔离的好处:
- 对于接口的实现类来说,实现的每个方法都是客户端所需要的,不存在逻辑上的方法浪费,实现代价小,相应的实现风险也会降低(通常,规模越大的类,越容易出现代码缺陷).
- 对客户端而言,接口暴露出的所有方法都是合理,即客户端不会对接口中的方法感到困惑.比如客户端只需要test1()方法,结果却发现接口中还提供了test2(),test3()…等,这样会造成客户端意外的使用风险,即不使用了不恰当的方法.
- 接口越小,也就意味着更加灵活.接口可以很容易的添加方法,但是如果想要删除,就要麻烦许多.比如,我们在实际生活中,我们用不同的画笔涂在一些可以混合成其他的颜色,但是要想将这种混合成的颜色在分开,那简直就是灾难.
最终不难发现,接口隔离能够实现有效的解耦,灵活性更高.
到现在,你可能还是感觉很抽象,现在让我们从具体的代码开始,去看看接口隔离在实际中的应用.
对于android的开发者而言,近乎所有的人都是用图片加载框架:ImageLoaderI.大部分情况下,我们需要知道图片是否开始下载,是否下载完成,是否下载失败,所以框架为我们提供了ImageLoadingListener这个接口:
public interface ImageLoadingListener {
void onLoadingStarted(String imageUri, View view);
void onLoadingFailed(String imageUri, View view, FailReason failReason);
void onLoadingComplete(String imageUri, View view, Bitmap loadedImage);
void onLoadingCancelled(String imageUri, View view);
}
通过实现该接口,我们便可以获得相关的下载状态.通常来说,这对我们来说已经足够了.但是在某些情况下,我们需要知道当前图片的下载进度,比如显示加载进度条等.这时我们发现ImageLoadingListener这个接口并没有为我们提供这样的一个方法.难道说ImageLoader的设计者忘记了?实则不然.ImageLoader中还为我们提供了ImageLoadingProgressListener接口:
public interface ImageLoadingProgressListener {
void onProgressUpdate(String imageUri, View view, int current, int total);
}
该接口非常简单,只含有onProgressUpdate()这一个方法,当你需要知道图片下载进度时,只需要提供该接口的实现类即可.
现在我们来考虑Imageloader的设计者为什么要将onProgressUpdate()单独定义成一个接口呢?
让我们站在开发者的角度想一下:大多数情况下,作为开发者我们只关注图片下载的成功与否.极少情况下,我们会关注下载的进度.如果我们将ImageLoadingListener和ImageLoadingProgressListener合二为一,如:
public interface ImageLoadingListenerTest {
void onLoadingStarted(String imageUri, View view);
void onLoadingFailed(String imageUri, View view, FailReason failReason);
void onLoadingComplete(String imageUri, View view, Bitmap loadedImage);
void onLoadingCancelled(String imageUri, View view);
void onProgressUpdate(String imageUri, View view, int current, int total);
}
那么这意味这,每提供一个ImageLoadingListenerTest的实现类,我们都需要实现这个很少用的onProgressUpdate()
,不但如此,你会发现onProgressUpdate()
关注的是下载过程中的持续状态,而其他几个方法则只关注下载中的几个瞬间(开始,取消,完成,失败),更重要的是下载过程相比几个瞬间通常意味着要做更多的考虑.借用八二法则来描述就是,极少用(20%)的onProgressUpdate()
比经常用的(80%)的onLoadingstarted()
,onLoadingFailed
,onLoadingComplete
,onLoadingCancelled
要花费更多的精力.
因此ImageLoader的设计者很明智的将其onProgressUpdate()
单独成一个接口,即将ImageLoadingListenerTest拆分成ImageLoadingListener和ImageLoadingProgressListener两个接口.
到目前为止,想必你已经了解了接口隔离的优点:建立单一的接口,尽可能的细化接口,让接口中含有的方法尽可能的少,以便提高系统的灵活性.
这里有人不禁问:到底要让接口有多小才算合理?里边含有一个方法,还是两个?还是?
这里有个简单的方法能帮助你确定具体划分成多少个:满足客户端的需求即可.客户端需要两个,那么这个接口就提供两个,如果你发现这两个方法能够被合并,那么就提供合并后的一个方法.当你无法判断这两个方法能不能合并时,那么就保持现状.
另外,如果你发现现有接口内聚性较低,那么尝试将这个该接口进行拆分,以需求高内聚,接口功能单一.
需要注意,如果只求接口内方法数量数量最少,那么很容易造成接口泛滥,导致系统设计反而更加复杂.
总之,接口隔离需要我们综合考量,才能做出合理的选择.
单一职责强调类的功能尽可能单一,而接口隔离则注重对接口依赖的隔离.
单一职责是从实现的角度约束的是类和方法,而接口隔离则是从抽象的角度约束接口.
都强调内聚性,即类和接口有各自的主题.