设计模式之设计原则-接口隔离原则
Posted 阿C_C
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式之设计原则-接口隔离原则相关的知识,希望对你有一定的参考价值。
接口隔离原则的定义
既然是接口隔离原则,那我们的主角就是接口了,在这里,接口分为两种:实例接口和类接口。
接口分类
实例接口
实例接口指的是Java中的类,你没看错,Java中的类也是接口的一种,我们知道,接口的本质是抽象,在于制定规范。例如,我们声明一个Person类,然后使用Person zhangsan = new Person();
产生了一个该类的实例,那么这个zhangsan的实例,就必须要遵从Person这个类的规范或者标准,比如,实例必须拥有类中的成员变量以及成员方法等。从这个角度来看,类也是一种接口。
类接口
注意,这里的类接口不要和上面说的类是接口的一种搞混淆了,这里的类接口,指的是使用Java中的interface关键字定义的接口,这个应该不难理解。
接口隔离原则的定义
- 客户端不应该依赖于不需要的接口
- 类之间的依赖关系,应该建立在最小的接口之上
第一句话,说的是客户端需要什么接口,就提供什么接口,人家不需要的,就不要提供出来。这就要求接口要仔细分析,保证少而精,不要提供臃肿不堪的“万能肥猪”接口。第二句和第一句是一个道理,能依赖小接口,就不要依赖大接口,比如客户端只需要发动机,就不要提供整车,接口要细化,纯洁,需要多大的东西,就给多大的东西。
总结起来,意思就是说:建立单一的接口,而不是臃肿庞大的接口,接口尽量细化,接口中的方法尽量要少,少到正好够客户端使用。这里需要和单一职责原则划分清楚,单一职责原则的要求是类和接口的职责要单一,注意是职责,是站在业务逻辑上进行责任划分的角度上来说的,而接口隔离原则要求的是接口中的方法尽量要少。例如定义一个用户管理的接口,该接口中有十几个方法,有供客户端调用的,有供第三方调用的,也有供前端WEB调用的,这个接口在单一职责原则看来是合理的,因为他只负责用户模块的功能,但是,在接口隔离原则看来,他负责了多个模块的功能,是不可以的,要尽量细化,该给谁调用的接口就给谁调用,不允许别的客户端来访问,要做到接口纯洁,专口专用,还不明白的话,想想一夫一妻制度:)。
实例说明
我们以星探寻找美女的例子,举例说明接口隔离原则的场景。
先定义抽象的星探类和美女接口,加入星探对美女的要求是长相好,身材好,气质好,则有如下类图:
根据该类图,我们可以写出美女以及星探的接口及其实现类:
接口:
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 + "---身材非常棒!");
星探抽象类:
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接口是否做到了最优化设计?没有!还可以进一步优化。
为什么这样说呢?我们知道人的审美随着时间地点等因素会变的,唐朝的肥女人放到现在可不算美,日本的僵尸艺妓搁咱这儿恐怕没几个人喜欢,现在的气质型美女也算是美女,那这个IPettyGirl接口就不适用了,到这里我们就发现,IPettyGirl接口设计过于庞大,把所有的性质都放在这一个接口中,那就要求满足所有条件的才算是美女,封装过度了,那我们需要想办法重新设计一下。
我们把原IPettyGirl接口拆分为两个接口,一种是外形美的美女IGoodBodyGirl,这类美女的特点就是脸蛋和身材极棒,超一流,但是没有审美素质,比如随地吐痰,文化程度比较低;另外一种是气质美的美女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依赖两个专用的接口比依赖一个综合的接口要灵活。接口是我们设计时对外
提供的契约,通过分散定义多个接口,可以预防未来变更的扩散,提高系统的灵活性和可维
护性。
接口纯洁性
接口隔离原则的目的是为了对接口进行规范和约束,其包含四层含义:
接口要尽量小
接口隔离原则的核心定义就是要求接口要尽量小,不出现臃肿接口,但是不能一直小下去,最低限度就是不能违反单一职责原则,也就是说,接口拆分到每个接口负责一个职责,如果继续向下拆分,子子孙孙何其多也,什么时候是个头?
拿之前的IPhone打电话和挂断的例子来说:
我们分析一下IConnectionManager是否还能继续拆分下去,当然可以,单挂电话就有两种方式,一种是正常挂断电话,一种是异常挂断,这两种方式的处理也应该不同,正常挂断电话,对方也会收到挂断信号,计费系统就自行断开了,异常挂断电话,属于信号丢失,中继服务器检查到之后才会通知计费系统停止计费。
那我们需要把挂断这个接口拆分成两个吗?且慢,我们回想一下IConnectionManager这个接口所负担的职责,不就是连接的建立和断开吗?再继续向下拆分,就不符合单一职责原则了,你还要它去关心更详细的业务和逻辑的处理,3/4G协议分析,中继服务器操作等,这样的话那就设计不下去了,这样的设计会陷入死循环的,It‘s a bad design! 那一个接口要拆分,还是不要拆分,以什么为原则呢?单一职责原则。
接口需要高内聚
所谓的高内聚,就是提高单一接口,类,模块的处理能力,尽量减少额外的交互,就可以独立的完成任务。比如你告诉下属“到奥巴马的办公室偷一个×××文件”,然后听到下属用坚定的口吻回答你:“是,保证完成任务!”一个月后,你的下属还真的把×××文件放到你的办公桌上了,这种不讲任何条件、立刻完成任务的行为就是高内聚的表现。具体到接口隔离原则,就是尽量减少public方法的公布,public方法是对外的承诺或者任务接口,公布的越少,对系统开发越有利,对后续的变更风险也会减少,同时也能降低成本。
定制服务
系统或者系统内的模块必然会有耦合,有耦合就要有互相访问的接口,我们设计的时候就要为各个访问者定制服务,所谓定制服务就是为每个访问个体提供优良服务,定制服务必然有一个要求,就是只提供访问者必要的方法,举个例子,图书管理系统,其中有一个查询接口,方便管理员查询图书,其类图如下所示:
在接口中定义了多个查询方法,分别可以按照作者、标题、出版社、分类进行查询,最后还提供了混合查询方式。程序写好了,投产上线了,突然有一天发现系统速度非常慢,然后就开始痛苦地分析,最终发现是访问接口中的complexSearch(Map map)方法并发量太大,导致应用服务器性能下降,然后继续跟踪下去发现这些查询都是从公网上发起的,进一步分析,找到问题:提供给公网(公网项目是另外一个项目组开发的)的查询接口和提供给系统内管理人员的接口是相同的,都是IBookSearcher接口,但是权限不同,系统管理人员可以通过接口的complexSearch方法查询到所有的书籍,而公网的这个方法是被限制的,不返回任何值,在设计时通过口头约束,这个方法是不可被调用的,但是由于公网项目组的疏忽,这个方法还是公布了出去,虽然不能返回结果,但是还是引起了应用服务器的性能巨慢的情况发
生,这就是一个臃肿接口引起性能故障的案例。
问题找到了,就需要把这个接口进行重构,将IBookSearcher拆分为两个接口,分别为两个模块提供定制服务,修改后的类图如下图:
提供给管理人员的实现类同时实现了ISimpleBookSearcher和IComplexBookSearcher两个接口,原有程序不用做任何改变,而提供给公网的接口变为ISimpleBookSearcher,只允许进行简单的查询,单独为其定制服务,减少可能引起的风险。
接口设计是有限度的
接口的设计粒度越小,系统越灵活,这是不争的事实。但是,灵活的同时也带来了结构的复杂化,开发难度增加,可维护性降低,这不是一个项目或产品所期望看到的,所以接口设计一定要注意适度,这个“度”如何来判断呢?根据经验和常识以及实际的业务场景判断,没有一个固化或可测量的标准。
最佳实践
接口隔离原则是对类以及接口的定义和约束,类和接口尽量使用原子接口或者原子类进行组装,但是如何划分原子一直是设计模式中的一大难题,我们在实践中总结了一下规则以供参考:
- 一个接口只服务于一个子模块或者业务逻辑
- 通过业务逻辑压缩接口中的public方法,时常去回顾整理,保证接口不会过于臃肿
- 已经被污染的接口要尽量修改,如果变动的风险较大,可以考虑使用适配器模式进行转化
- 了解实际的业务场景,根据实际状况进行设计,切莫盲从,不要看到被人这么设计就去跟随,每个项目都有特定的场景或者特点,环境不同,拆分标准就不同,只有根据实际场景才能做出最合适的设计。
所有设计原则都一样,都需要花费较多的时间和精力进行设计和筹划,带来的设计灵活性可以轻松应付业务人员的各种要求。虽然说,贯彻接口隔离原则最好的方法就是一个接口一个方法,但是真的会这么做吗?肯定不会。那怎么才能正确地使用接口隔离原则呢?答案是根据经验,常识,实际业务场景,决定接口的粒度大小,粒度太小,接口数量剧增,累死开发人员,接口粒度太大,灵活性降低,无法满足业务人员要求,并且会带来风险。
怎么准确地实践接口隔离原则?实践、经验和领悟!
以上是关于设计模式之设计原则-接口隔离原则的主要内容,如果未能解决你的问题,请参考以下文章