本文参考自设计模式之禅(第二版)第四章
1 接口隔离原则的定义
在讲接口隔离原则之前,先明确一下我们的主角—接口。接口分为两种:
- 实例接口(Object Interface),在Java中声明一个类,然后用new关键字产生一个实例,它是对一个类型的事物的描述,这是一种接口。比如你定义Person这个类,然后使用Person zhangSan = new Person()产生了一个实例,这个实例要遵从的标准就是Person这个类,Person类就是zhangSan的接口。疑惑?看不懂?不要紧,那是因为让Java语言浸染的时间太长了,只要知道从这个角度来看,Java中的类也是一种接口。
- 类接口(Class Interface),Java中经常使用的interface关键字定义的接口。
主角已经定义清楚了,那什么是隔离呢?它有两种定义,如下所示:
- 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.(类间的依赖关系应该建立在最小的接口上。)
新事物的定义一般都比较难理解,晦涩难懂是正常的。我们把这两个定义剖析一下,先说第一种定义:“客户端不应该依赖它不需要的接口”,那依赖什么呢?依赖它需要的接口,客户端需要什么接口就提供什么接口,把不需要的接口剔除掉,那就需要对接口进行细化,保证其纯洁性;再看第二种定义:“类间的依赖关系应该建立在最小的接口上”,它要求是最小的接口,也是要求接口细化,接口纯洁,与第一个定义如出一辙,只是一个事物的两种不同描述。
我们可以把这两个定义概括为一句话:建立单一接口,不要建立臃肿庞大的接口。再通俗一点讲:接口尽量细化,同时接口中的方法尽量少。看到这里大家有可能要疑惑了,这与设计原则—单一职责原则不是相同的吗?错,接口隔离原则与单一职责原则的审视角度是不相同的,单一职责原则要求的是类和接口的职责单一,注重的是职责,这是业务逻辑上的划分,而接口隔离原则要求接口的方法尽量少。例如一个接口的职责可能包含10个方法,这10个方法都放在一个接口中,并且提供给多个模块访问,各个模块按照规定的权限来访问,在系统外通过文档约束“不使用的方法不要访问”,按照单一职责原则是允许的,按照接口隔离原则是不允许的,因为它要求“尽量使用多个专门的接口”。专门的接口指什么?就是指提供给每个模块的都应该是单一接口,提供给几个模块就应该有几个接口,而不是建立一个庞大的臃肿的接口,容纳所有的客户端访问。
2 美女何其多,观点各不同
我们举例来说明接口隔离原则到底对我们提出了什么要求。现在男生对小姑娘的称呼,使用频率最高的应该是“美女”了吧,你在大街上叫一声“嗨,美女!”估计10个有8个回头,其中包括那位著名的如花。美女的标准各不相同,首先就需要定义一下什么是美女:首先要面貌好看,其实是身材要窈窕,然后要有气质,当然了,这三者各人的排列顺序不一样,总之要成为一名美女就必须具备:面貌、身材和气质,我们用类图体现一下星探(当然,你也可以把自己想象成星探)找美女的过程,如下图1所示:
定义了一个IPettyGirl接口,声明所有的美女都应该有goodLooking、niceFigure、greatTemperament,然后又定义了一个抽象类AbstractSearcher,起作用就是搜索美女并显示其信息,只要美女都按照这个规范定义,Searcher(星探)就轻松多了,美女类的实现如下代码1所示:
public interface IPettyGirl {
public void goodLooking();
public void niceFigure();
public void greatTemperament();
}
美女的标准定义完毕,具体的美女实现类如下代码2所示:
public class PettyGirl implements IPettyGirl {
private String name;
//美女都有名字
public PettyGirl(String _name) {
this.name = _name;
}
public void goodLooking() {
System.out.println(this.name + "---脸蛋很漂亮");
}
public void niceFigure() {
System.out.println(this.name + "---身材非常棒");
}
public void greatTemperament() {
System.out.println(this.name + "---气质非常好");
}
}
通过三个方法,把对美女的要求都定义了出来,按照这个标准,如果姑娘被排除在美女转标准之外了。有美女,就有搜索美女的星探,其具体实现如下代码3所示:
public abstract class AbstractSearcher {
protected IPettyGirl pettyGirl;
public AbstractSearcher(IPettyGirl _pettyGirl) {
this.pettyGirl = _pettyGirl;
}
public abstract void show();
}
星探的实现类就比较简单了,其源代码如下代码4所示:
public class Searcher extends AbstractSearcher {
public Searcher(IPettyGirl _pettyGirl) {
super(_pettyGirl);
}
//展示美女的信息
public void show() {
System.out.println("-----美女的信息如下:-----");
//展示面容
super.pettyGirl.goodLooking();
//展示身材
super.pettyGirl.niceFigure();
//展示气质
super.pettyGirl.greatTemperament();
}
}
场景中的两个角色美女和星探都已经出现了,需要写一个场景类来串联起各个角色,场景类的实现如下代码5所示:
public class Client {
//搜索并展示美女信息
public static void main(String[] args) {
//定义一个美女
IPettyGirl lingling = new PettyGirl("玲玲");
AbstractSearcher searcher = new Searcher(lingling);
searcher.show();
}
}
星探搜索美女的运行结果如下:
-----美女的信息如下:-----
玲玲---脸蛋很漂亮
玲玲---身材非常棒
玲玲---气质非常好
星探寻找美女的程序开发完毕了,运行结果也是正确的。我们回头来想想这个程序有没有问题,思考一下IPettyGirl这个接口,这个接口是否做到了最优设计?答案是没有,还可以对接口进行优化。
我们的审美观点都在改变,美女的定义也在变化。唐朝的杨贵妃如果活在现在这个年代非得羞愧而死不可,为什么?胖呀!但是胖并不影响她入选中国四大美女,说明但是的审美观与现在是有差异的。当然,随着时代的发展,我们的审美观也在变化,当你发现一个女孩,脸蛋不怎么样,身材以一般般,但是气质非常好,我相信大部分的人都会把这样的女孩叫美女,审美素质提升了,就产生了气质型美女,但是我们的接口却定义了美女必须是三者都具备,按照这个标准,气质美女就不算美女,那怎么办?可能你要说了,我重新扩展一个美女类,只实现greatTemperament方法,其他两个方法置空,什么都不写,不就可以了吗?聪明,但是行不通!为什么呢?星探AbstractSearcher依赖的是IPettyGirl接口,它有三个方法,你只实现了一个方法,星探的方法是不是要修改?我们上面的程序打印出来的信息少了两条,还让星探怎么去辨别是不是美女呢?
分析到这里,我们发现接口IPettyGril的设计是有缺陷的,过于庞大了,容纳了一些可变的因素,根据接口隔离原则,星探AbstractSearcher应该依赖于具有部分特质的女孩子,而我们却把这些特质都封装了起来,放到了一个接口中,封装过度了!问题找到了,我们重新设计一下类图,修改后的类图如下图2所示:
把原IPettyGirl接口拆分成两个接口,一种是外形美的美女IGoodBodyGirl,这种美女的特点就是脸蛋和身材极棒,超一流,但是没有审美素质,比如随地吐痰,文化程度比较低;另外一种是气质美的美女IGreatTemperamentGirl,谈吐和修养都非常高。我们把一个比较臃肿的接口拆分成了两个专门的接口,灵活性提高了,可维护性也增加了,不管以后是要外形美的美女还是气质美的美女都可以轻松地通过PettyGirl定义。两种类型的美女定义如下代码6所示:
public interface IGoodBodyGirl {
//要有姣好的面孔
public void goodLooking();
//要有好身材
public void niceFigure();
}
public interface IGreatTemperament {
//要有气质
public void greatTemperament();
}
按照脸蛋、身材、气质都具备才算美女,实现类实现两个接口,如下代码7所示:
public class PettyGirl implements IGoodBodyGirl, IGreatTemperamentGirl {
private String name;
//美女都有名字
public PettyGril(String _name) {
this.name = _name;
}
public void goodLooking() {
System.out.println(this.name + "---脸蛋很漂亮");
}
public void niceFigure() {
System.out.println(this.name + "---身材非常棒");
}
public void greatTemperament() {
System.out.println(this.name + "---气质非常好");
}
}
通过这样的重构以后,不管以后是要气质美女还是要外形美女,都可以保持接口的稳定。当然,你可能要说了,以后可能审美观点再发生改变,只要脸蛋好看就是美女,那这个IGoodBody接口还是要修改的呀,确实是,但是设计是有限度的,不能无限的考虑未来的变更情况,否则就会陷入设计的泥潭中而不能自拔。
以上把一个臃肿的接口变更为两个独立的接口所依赖的原则就是接口隔离原则,让星探AbstractSearcher依赖两个专用的接口比依赖一个综合的接口要灵活。接口是我们设计时对外提供的契约,通过分散定义多个接口,可以预防未来变更的扩散,提高系统的灵活性和可维护性。
3 保证接口的纯洁性
接口隔离原则是对接口进行规范约束,其包含以下4层含义:
- 接口要尽量小
- 接口要高内聚
- 定制服务
- 接口设计是有限度的
4 最佳实践
接口隔离原则是对接口的定义,同时也是对类的定义,接口和类尽量使用原子接口或原子类来组装。但是,这个原子该怎么划分是设计模式中的一大难题,在时间中可以根据以下几个规则来衡量:
- 一个接口只服务于一个子模块或业务逻辑
- 通过业务逻辑亚索接口中的public方法,接口时常去回顾,尽量让接口达到“满身筋骨肉”,而不是“肥嘟嘟”的一大堆方法;
- 已经被污染了的接口,尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理;
- 了解环境,拒绝盲从。每个项目或产品都有特定的环境因素,别看到大师是这样做的你就照抄。千万别,环境不同,接口拆分的标准就不同。深入了解业务逻辑,最好的接口设计就出自你手中!