四、接口隔离原则
《一、单一职责原则》
《二、里氏替换原则》
《三、依赖倒置原则》
《四、接口隔离原则》
《五、迪米特法则》
《六、开闭原则》
4.1、接口隔离原则的定义
在讲接口隔离原则之前,我们先明确一下我们的主角,什么是接口,接口分为两种:
一种是实例接口 (Object Interface),在 Java 中声明一个类,然后用 new 关键字产生的一个实例,它是对一个类型的事 物描述,这是一种接口,比如你定义个 Person 这个类,然后使用 Person zhangSan = new Person()产生了 一个实例,这个实例要遵从的标准就是 Person 这个类,Person 类就是 zhangSan 的接口,看不懂?不要紧, 那是让 Java 语言浸染的时间太长了。主角已经出场了,那我们看它的原则是什么,它有两种定义:
第一种定义: 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 个方法都放在一个接口 中,并且提供给多个模块访问,各个模块按照规定的权限来访问,在系统外通过文档约束不使用的方法不 要访问,按照单一职责原则是允许的,按照接口隔离原则是不允许的,因为它要求“尽量使用多个专门的 接口”,专门的接口指什么?就是指提供给多个模块的接口,提供给几个模块就应该有几个接口,而不是建 立一个庞大的臃肿的接口,所有的模块可以来访问。
未遵循接口隔离原则的设计:
遵循接口隔离原则的设计:
根据接口隔离原则拆分接口时,必须首先满足单一职责原则。
我们来举个例子来说明接口隔离原则到底对我们提出了什么要求。现在男生对小姑娘的称呼使用频率 最高的应该是“美女”了吧,我们今天来定义一下什么是美女:首先要面貌好看,其次是身材要窈窕,然 后要有气质,当然了,这三者各人的排列顺序不一样,总之要成为一名美女就必须具备:面貌、身材和气质,我们用类图类体现一下星探(当然,你也可以把你自己想想成星探)找美女的过程,看类图:
定义了一个 IPettyGirl 接口,声明所有的美女都应该有 goodLooking、niceFigure 和 greatTemperament,然后又定义了一个抽象类 AbstractSearcher,其作用就是搜索美女然后展示信息,只 要美女都是按照这个规范定义,Searcher(星探)就轻松的多了,我们先来看美女的定义:
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 Client { //搜索并展示美女信息 public static void main(String[] args) { //定义一个美女 IPettyGirl yanYan = new PettyGirl(" 嫣嫣"); AbstractSearcher searcher = new Searcher(yanYan); searcher.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(); } }
运行结果如下:
--------美女的信息如下: --------------- 嫣嫣---脸蛋很漂亮! 嫣嫣---身材非常棒! 嫣嫣---气质非常好!
采用接口隔离原则:重新修改一下类图:
把原 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 依 赖两个专用的接口比依赖一个综合的接口要灵活。接口是我们设计时对外提供的契约,通过分散定义多个 接口,可以预防未来变更的扩散,提高系统的灵活性和可维护性。
4.3 保证接口的纯洁性
接口隔离原则是对接口进行规范约束,其包含以下4层含义:
● 接口要尽量小
这是接口隔离原则的核心定义,不出现臃肿的接口(Fat Interface),但是“小”是有限度的,首先就是不能违反单一职责原则,什么意思呢?我们在单一职责原则中提到一个IPhone
的例子,在这里,我们使用单一职责原则把两个职责分解到两个接口中,类图如下所示:
仔细分析一下IConnectionManager接口是否还可以再继续拆分下去,挂电话有两种方式:一种是正常的电话挂断,一种是电话异常挂机,比如突然没电了,通信当然就断了。这
两种方式的处理应该是不同的,为什么呢?正常挂电话,对方接受到挂机信号,计费系统也就停止计费了,那手机没电了这种方式就不同了,它是信号丢失了,中继服务器检查到了,
然后通知计费系统停止计费,否则你的费用不是要疯狂地增长了吗?
思考到这里,我们是不是就要动手把IConnectionManager接口拆封成两个,一个接口是负责连接,一个接口是负责挂电话?是要这样做吗?且慢,让我们再思考一下,如果拆分
了,那就不符合单一职责原则了,因为从业务逻辑上来讲,通信的建立和关闭已经是最小的业务单位了,再细分下去就是对业务或是协议(其他业务逻辑)的拆分了。想想看,一个电
话要关心3G协议,要考虑中继服务器,等等,这个电话还怎么设计得出来呢?从业务层次来看,这样的设计就是一个失败的设计。一个原则要拆,一个原则又不要拆,那该怎么办?
好办,根据接口隔离原则拆分接口时,首先必须满足单一职责原则。
● 接口要高内聚
什么是高内聚?高内聚就是提高接口、类、模块的处理能力,减少对外的交互。比如你告诉下属“到奥巴马的办公室偷一个×××文件”,然后听到下属用坚定的口吻回答你:“是,保
证完成任务!”一个月后,你的下属还真的把×××文件放到你的办公桌上了,这种不讲任何条件、立刻完成任务的行为就是高内聚的表现。具体到接口隔离原则就是,要求在接口中尽量少公布public方法,接口是对外的承诺,承诺越少对系统的开发越有利,变更的风险也就越少,同时也有利于降低成本。
● 定制服务
一个系统或系统内的模块之间必然会有耦合,有耦合就要有相互访问的接口(并不一定就是Java中定义的Interface,也可能是一个类或单纯的数据交换),我们设计时就需要为各
个访问者(即客户端)定制服务,什么是定制服务?定制服务就是单独为一个个体提供优良的服务。我们在做系统设计时也需要考虑对系统之间或模块之间的接口采用定制服务。采用
定制服务就必然有一个要求:只提供访问者需要的方法,这是什么意思?我们举个例子来说明,比如我们开发了一个图书管理系统,其中有一个查询接口,方便管理员查询图书,其类
图如下图所示:
在接口中定义了多个查询方法,分别可以按照作者、标题、出版社、分类进行查询,最后还提供了混合查询方式。程序写好了,投产上线了,突然有一天发现系统速度非常慢,然
后就开始痛苦地分析,最终发现是访问接口中的complexSearch(Map map)方法并发量太大,导致应用服务器性能下降,然后继续跟踪下去发现这些查询都是从公网上发起的,进一步分析,找到问题:提供给公网(公网项目是另外一个项目组开发的)的查询接口和提供给系统内管理人员的接口是相同的,都是IBookSearcher接口,但是权限不同,系统管理人员可以通过接口的complexSearch方法查询到所有的书籍,而公网的这个方法是被限制的,不返回任何值,在设计时通过口头约束,这个方法是不可被调用的,但是由于公网项目组的疏忽,这个方法还是公布了出去,虽然不能返回结果,但是还是引起了应用服务器的性能巨慢的情况发生,这就是一个臃肿接口引起性能故障的案例。
问题找到了,就需要把这个接口进行重构,将IBookSearcher拆分为两个接口,分别为两个模块提供定制服务,修改后的类图如下所示:
提供给管理人员的实现类同时实现了ISimpleBookSearcher和IComplexBookSearcher两个接口,原有程序不用做任何改变,而提供给公网的接口变为ISimpleBookSearcher,只允许进行简单的查询,单独为其定制服务,减少可能引起的风险。
● 接口设计是有限度的
接口的设计粒度越小,系统越灵活,这是不争的事实。但是,灵活的同时也带来了结构的复杂化,开发难度增加,可维护性降低,这不是一个项目或产品所期望看到的,所以接口
设计一定要注意适度,这个“度”如何来判断呢?根据经验和常识判断,没有一个固化或可测量的标准。
最佳实践
接口隔离原则是对接口的定义,同时也是对类的定义,接口和类尽量使用原子接口或原子类来组装。但是,这个原子该怎么划分是设计模式中的一大难题,在实践中可以根据以下几个规则来衡量:
● 一个接口只服务于一个子模块或业务逻辑;
● 通过业务逻辑压缩接口中的public方法,接口时常去回顾,尽量让接口达到“满身筋骨 肉”,而不是“肥嘟嘟”的一大堆方法;
● 已经被污染了的接口,尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理;
● 了解环境,拒绝盲从。每个项目或产品都有特定的环境因素,别看到大师是这样做的你就照抄。千万别,环境不同,接口拆分的标准就不同。深入了解业务逻辑,最好的接口设计就 出自你的手中!
接口隔离原则和其他设计原则一样,都需要花费较多的时间和精力来进行设计和筹划,但是它带来了设计的灵活性,让你可以在业务人员提出“无理”要求时轻松应付。贯彻使用接
口隔离原则最好的方法就是一个接口一个方法,保证绝对符合接口隔离原则(有可能不符合单一职责原则),但你会采用吗?不会,除非你是疯子!那怎么才能正确地使用接口隔离原
则呢?答案是根据经验和常识决定接口的粒度大小,接口粒度太小,导致接口数据剧增,开发人员呛死在接口的海洋里;接口粒度太大,灵活性降低,无法提供定制服务,给整体项目
带来无法预料的风险。
怎么准确地实践接口隔离原则?实践、经验和领悟!