声明:本文内容是从网络书籍整理而来,并非原创。
第一种定义:
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 个方法都放在一个接口中,并且提供给多个模块访问,各个模块按照规定的权限来访问,在系统外通过文档约束不使用的方法不要访问,按照单一职责原则是允许的,按照接口隔离原则是不允许的,因为它要求“尽量使用多个专门的接口”,专门的接口指什么?就是指提供给多个模块的接口,提供给几个模块就应该有几个接口,而不是建立一个庞大的臃肿的接口,所有的模块可以来访问。
我们来定义一下什么是美女:1.面貌好看,2.身材要窈窕,3.要有气质。我们用类图类体现一下星探找美女的过程:
美女的定义:
public interface IPettyGirl {
//要有姣好的面孔
public void goodLooking();
//要有好身材
public void niceFigure();
//要有气质
public void greatTemperament();
}
美女的实现类:
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 greatTemperament() {
System.out.println(this.name + "---气质非常好!");
}
//身材要好
public void niceFigure() {
System.out.println(this.name + "---身材非常棒!");
}
}
然后我们来看 AbstractSearcher 类,这个类就是指星探这个行业了,代码如下:
public abstract class AbstractSearcher {
protected IPettyGirl pettyGirl;
public AbstractSearcher(IPettyGirl _pettyGirl){
this.pettyGirl = _pettyGirl;
}
//搜索美女,列出美女信息
public abstract void show();
}
星探查找到美女,打印出美女的信息,代码如下:
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();
}
}
场景中的两个角色美女和星探都已经完成了,我们再来写个场景类,展示一下我们的这个过程:
public class Client {
//搜索并展示美女信息
public static void main(String[] args) {
//定义一个美女
IPettyGirl yanYan = new PettyGirl("嫣嫣");
AbstractSearcher searcher = new Searcher(yanYan);
searcher.show();
}
}
运行结果如下:
--------美女的信息如下: ---------------
嫣嫣---脸蛋很漂亮!
嫣嫣---身材非常棒!
嫣嫣---气质非常好!
星探寻找美女的程序我们就开发完毕了,我们来想想这个程序有没有问题,思考一下 IPettyGirl 这个接口,这个接口是否做到了最优秀的设计。
我们的审美观点都在改变,美女的定义也在变化。一千多年前的唐朝杨贵妃如果活在现代这个年代非羞愧死不行,为什么?胖呀!但是胖不不影响她入选中国的四大美女行列,说明当时的审美和现在是有差异地,当然随着时代的发展我们的审美观也在变化,就现在,你发现有一个女孩,脸蛋不怎么样,身材也一般般,但是气质非常好,大部分人也会把这样的女孩叫美女,审美素质提升了,但是我们接口却定义了美女必须是三者都具备呀,可能你要说了,我重新扩展一个美女类,只实现 greatTemperament 方法其他两个方法置空,什么都不写,不就可以了吗?聪明,但是行不通!为什么呢?星探 AbstractSearcher 依赖的是 IPettyGirl 接口,它有三个方法,你只实现了两个方法,星探的方法是不是要修改?我们上面的程序打印出来的信息少了两条,还让星探怎么去辨别是不是美女呢?好了,我们发现我们的接口 IPettyGirl 接口设计是有缺陷地,过于庞大了,容纳了一些可变的因素,根据接口隔离原则,星探 AbstractSearcher 应该依赖与具有部分特质的女孩子,而我们却把这些特质都封装了起来,放到了一个接口中了,封装过渡了!问题查找到了,我们重新修改一下类图:
把原 IPettyGirl 接口拆分为两个接口,一种是外形美的美女 IGoodBodyGirl,这类美女的特点就是脸蛋和身材极棒,超一流,但是没有审美素质,比如随地吐痰,出口就是 KAO,CAO 之类的,文化程度比较低;另外一种是气质美的美女 IGreatTemperamentGirl,谈吐和修养都非常高。我们从一个比较臃肿的接口拆分成了两个专门的接口,灵活性提高了,可维护性也增加了,不管以后是要外形美的美女还是气质美的美女都可以轻松的通过 PettyGirl 定义。我们先看两种类型的美女接口:
public interface IGoodBodyGirl {
//要有姣好的面孔
public void goodLooking();
//要有好身材
public void niceFigure();
}
public interface IGreatTemperamentGirl {
//要有气质
public void greatTemperament();
}
实现类没有改变,只是实现类两个接口,代码如下:
public class PettyGirl implements IGoodBodyGirl,IGreatTemperamentGirl {
private String name;
//美女都有名字
public PettyGirl(String _name){
this.name=_name;
}
//脸蛋漂亮
public void goodLooking() {
System.out.println(this.name + "---脸蛋很漂亮!");
}
//气质要好
public void greatTemperament() {
System.out.println(this.name + "---气质非常好!");
}
//身材要好
public void niceFigure() {
System.out.println(this.name + "---身材非常棒!");
}
}
通过这样的改造以后,不管以后是要气质美女还是要外形美女,都可以保持接口的稳定。当然你可能要说了,以后可能审美观点再发生改变,只有脸蛋好看就是美女,那这个 IGoodBody 接口还是要修改的呀,确实是,但是设计时有限度的,不能无限的考虑未来的变更情况,否则就会陷入设计的泥潭中而不能自拔。
以上把一个臃肿的接口变更为两个独立的接口依赖的原则就是接口隔离原则,让 AbstractSearcher 依赖两个专用的接口比依赖一个综合的接口要灵活。接口是我们设计时对外提供的契约,通过分散定义多个接口,可以预防未来变更的扩散,提高系统的灵活性和可维护性。
接口尽量要小。这是接口隔离原则的核心定义,不出现臃肿的接口(Fat Interface),但是“小”是有限度的,首先就是不能违反单一职责原则, 什么意思呢?我们在单一职责原则中提到一个 IPhone 的例子,在这里例子中我们使用单一职责原则把在一个接口中的方法分解到两个接口中,类图如下:
我们想想 IConnectManager 接口是否还可以再继续拆分下去,挂电话有两种方式:一种是正常的电话挂掉,一种是手机突然没电了,通讯当然就断了,这两种方式的处理应该是不同的,为什么呢?正常挂电话,对方接受到挂机信号,计费系统也就停止计费了,那手机没电了这种方式就不同了,它是信号丢失了,中继服务器检查到了,然后通知计费系统停止计费,否则你的费用不是要疯狂的增长了吗?!思考到这里,我们是不是就要动手把 IConnectManager 接口拆封成两个, 一个是负责连接的,一个接口是负责挂电话的?是要这样做吗?且慢,让我们再思考一下,如果拆分了,那就不符合单一职责原则了,因为从业务上来讲通讯的建立和关闭已经是最小的业务单位了,再细分下去就是对业务或是协议(其他业务逻辑)的拆解了,想想看一个电话要关心 3G 协议,要考虑中继服务器等等,这个电话还怎么做的出来呢?从业务层次来看这样的设计就是一个失败的设计。一个原则要拆,你原则又不要拆,那怎么办?好办, *根据接口隔离原则拆分接口时,必须首先满足单一职责原则。
接口要高内聚。 什么是高内聚?高内聚就是提高接口、类、模块的处理能力,减少对外的交互,就比如一个人,你告诉下属“到奥巴马的办公室偷一个 XX 文件”,然后就听到下属就坚定的口吻回答你“好的,保证完成!”,然后一个月后还真的把 XX 文件放到你的办公桌了,这种不讲任何条件、立刻完成任务的行为就是高内聚的表现。具体到接口隔离原则就是要求在接口中尽量少公布 public 方法,接口是对外的承诺,承诺越少对系统的开发越有利,变更的风险也就越少,同时也有利于降低成本。
定制服务。 一个系统或系统内的模块之间必然会有耦合,有耦合就要相互访问的接口(并不一定就是Java 中定义的 Interface,也可能是一个类或者是单纯的数据交换),我们设计时就需要给各个访问者(也就客户端)定制服务,什么是定制服务?你到商场买衣服,找到符合自己身体的尺码的衣服就成了,基本上就不会差别太大,可能是前松后紧,晚上睡不着觉之类的不太合适,但是好歹也是个衣服,能穿。如果你到裁缝店里做衣服会是什么样子呢?裁缝会帮你量腰围,胸围,肩宽等等,然后做出一件衣服,这件衣服肯定非常符合你的身体,那这就是定制服务,单独为一个个体提供优良优良的服务。我们在做系统设计时也需要考虑对系统之间或模块之间的定义要采用定制服务,采用定制服务就必然有一个要求就是:只提供访问者需要的方法,这是什么意思?我们举个例子来说明,比如我们做了一个图书管理系统,其中有一个查询接口的,方便管理员查询图书,其类图如下:
在接口中定义了多个方法,分别可以按照作者,标题,出版社,分类查询,最后还提供了混合查询方式,程序也写好了,投产上线了,突然有一天发现系统速度非常慢,然后就开始痛苦的分析,最终发现是通过这个接口的 complexSearch(Map map)方法并发量太大,导致应用服务器性能下降,然后继续跟踪下去才发现这些查询都是从公网上发起的,进一步分析,发现问题了:提供给公网(公网项目是另外一个项目组开发的)的查询接口和提供给系统内管理人员的接口是相同的,都是 IBookSearcher 接口,但是权限不同,系统管理人员可以通过这个接口查询到所有的书籍,而公网的这个方法是被限制的,不返回任何值的,在设计时通过口头约束,这个方法是不可被调用的,但是由于公网项目组的疏忽,这个方法还是公布了出去,虽然不能返回结果,但是还是引起了应用服务器的性能巨慢的情况发生,这就是活生生的一起接口臃肿引起性能部长的案例。
问题找到了,我们就把这个接口进行重构:把 IBookSearcher 拆分为两个接口,如下图:
提供给管理人员的实现类同时实现 ISimpleBookSearcher 和 IComplexBookSearcher 两个接口,原有程序不用任何改变,而提供给公网的接口变为 ISimpleBookSearcher,只允许进行简单的查询,单独为它定制服务。
接口设计是有限度的。接口的设计粒度是越小系统越灵活,这是不争的事实,但是这就带来的结构的复杂化,开发难度增加,维护性降低,这不是一个项目或产品所期望看到的,所有接口设计一定要注意适度,适度的“度”怎么来判断的呢?根据经验和常识判断!
接口隔离原则和其他的设计原则一样,都是需要花费较多的时间和精力来进行设计和筹划,但是它带来了设计的灵活性,让你在业务人员在提出“无理”要求的时候可以轻松应付。贯彻使用接口隔离原则最好的方法就是一个接口一个方法,保证绝对符合接口隔离原则(有可能不符合单一职责原则),但你会采用吗?!不会,除非你是疯子!那怎么才能正确的使用接口隔离原则呢? 答案是根据经验和常识决定接口的粒度大小,接口粒度太小,导致接口数据剧增,开发人员呛死在接口的海洋里;接口粒度太大,灵活性降低,无法提供定制服务,给整体项目带来无法预计的风险。
怎么准确的实践接口隔离原则?一句话:实践,经验和领悟!