接口隔离原则的定义

接口分为两种:

  • 实例接口(Object Interface),在Java中声明一个类,然后用new关键字产生一个实例。它是对一个类型的事物的描述,是一种接口。从这个角度,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个方法都放在一个接口中并提供给多个模块,各个模块按规定权限访问,在系统外通过文档约束“不使用的方法不要访问”。这按照单一职责原则是允许的,按照接口隔离原则则不允许,因为它要求“尽量使用多个专门的接口”,即提供给每个模块的都应该是单一接口,提供给几个模块就挺有几个接口,而非建立一个庞大而臃肿的接口容纳所有的客户端访问。


接口隔离原则的要求

通过举例可以说明接口隔离原则提出的要求。对于一个美女的定义,需要具备:面貌、身材和气质。下图为星探寻找美女的类图:
image.png
其中定义了一个IPettyGirl接口,声明所有美女都应具备goodLooking,niceFigure和greatTemperament。然后定义一个抽象类AbstractSearcher,起作用为搜索美女并显示其信息。美女类代码如下所示:

  1. public interface IPettyGirl {
  2. public void goodLooking();
  3. public void niceFigure();
  4. public void greatTemperament();
  5. }

美女实现类的代码如下:

  1. public class PettyGirl implements IPettyGirl {
  2. private String name;
  3. public PettyGirl(String _name) {
  4. this.name = _name;
  5. }
  6. public void goodLooking() {
  7. System.out.println(this.name + " has Beautiful Face");
  8. }
  9. public void greatTemperament() {
  10. System.out.println(this.name + " has Great Temperament");
  11. }
  12. public void niceFigure() {
  13. System.out.println(this.name + " has Nice Figure");
  14. }
  15. }

搜寻美女的星探抽象类代码如下:

  1. public abstract class AbstractSearcher {
  2. protected IPettyGirl prettyGirl;
  3. public AbstractSearcher(IPettyGirl _prettyGirl) {
  4. this.prettyGirl = _prettyGirl;
  5. }
  6. // 搜寻美女,列出信息
  7. public abstract void show();
  8. }

星探实现类代码如下:

  1. public class Searcher extends AbstractSearcher {
  2. public Searcher(IpettyGirl _prettyGirl) {
  3. super(_prettyGirl);
  4. }
  5. public void show() {
  6. System.out.println("------- Message -------");
  7. super.prettyGirl.goodLooking();
  8. super.prettyGirl.greatTemperament();
  9. super.prettyGirl.niceFigure();
  10. }
  11. }

场景类如下:

  1. public class Client {
  2. public static void main(String[] args) {
  3. IPettyGirl kitty = new PettyGirl("Kitty");
  4. AbstractSearcher searcher = new Searcher(kitty);
  5. searcher.show();
  6. }
  7. }

运行结果:

  1. ------- Message -------
  2. Kitty has Beautiful Face
  3. Kitty has Great Temperament
  4. Kitty has Nice Figure

星探寻找美女的程序如上文所示,但此程序中的接口IPettyGirl设计有一定缺陷。

接口定义了“美女”必须三个特点都具备,而如果只具备部分特点的“美女”则无法定义。如果扩展一个类只实现其中一个方法,将另两个方法置空,则会出现需要修改星探AbstractSearcher的情况(因为AbstractSearcher依赖IPettyGirl,而其有三个方法)。

因此,此接口过于庞大,容纳了一些可变因素。根据接口隔离原则,星探Abstract应该依赖于具有部分特质的女孩子,而我们却将所有特质都封装在了一个接口中。因此可以重新设计类图如下:
image.png
将IPettyGirl接口拆分成两个接口:外形美女IGoodBodyGirl和气质美女IGreatTemperamentGirl,这样就增加了灵活性和可维护性。其接口代码如下:

  1. public interface IGoodBodyGirl {
  2. public void goodLooking();
  3. public void niceFigure();
  4. }
  5. public interface IGreatTemperamentGirl {
  6. public void greatTemperament();
  7. }

按照脸蛋、身材、气质都具备才算美女,实现类实现两个接口,代码如下:

  1. public class PettyGirl implements IGoodBodyGirl, IGreatTemperamentGirl {
  2. private String name;
  3. public PettyGirl(String _name) {
  4. this.name = _name;
  5. }
  6. public void goodLooking() {
  7. System.out.println(this.name + " has Beautiful Face");
  8. }
  9. public void greatTemperament() {
  10. System.out.println(this.name + " has Great Temperament");
  11. }
  12. public void niceFigure() {
  13. System.out.println(this.name + " has Nice Figure");
  14. }
  15. }

重构后,不论气质美女还是外形美女都可以保持接口稳定,将一个臃肿的接口拆分成两个独立的接口,其依赖的原则就是接口隔离原则。让星探AbstractSearcher依赖两个专用接口比依赖一个综合的接口要灵活。 接口是设计时对外提供的契约,通过分散定义多个接口,可以预防未来变更的扩散,提高系统的灵活性和可维护性。


接口隔离原则的内在含义

接口隔离原则是对接口进行规范约束,其包含一下4层含义:

接口要尽量小

这是接口隔离原则的核心定义,即不出现臃肿接口(Fat Interface)。但是“小”也有一定限度,必须满足单一职责原则。

对于单一职责原则中的IPhone例子,其类图如下:
image.png
分析IConnectionManager接口,挂电话的方式有两种:正常挂断和电话异常挂机(如突然没电、没信号等)。两种挂断的处理方式不同,因此可以尝试将IConnectionManager接口拆分成两个,一个负责连接,一个负责挂断,但这样就违背了单一职责原则。从逻辑上通信的建立和关闭已经是最小业务单位了,过于细分的设计也是失败的设计。因此 对于接口的拆分,前提必须满足单一职责原则

接口要高内聚

高内聚即提高接口、类、模块的处理能力,减少对外交互。具体到接口隔离原则中,即要求在接口中尽量少公布public方法。接口是对外的契约,暴露的方法越少对系统的开发越有利,变更风险也越小,同时利于降低成本。

定制服务

定制服务:单独为一个个体提供优良的服务。一个系统或其内部的模块之间必然存在耦合,有耦合就必然后互相访问的接口(interface或单纯的数据交换),就需要为各个访问者(client)定制服务。同时在做系统设计时也要考虑对系统之间或模块之间的接口采用定制服务。

定制服务的要求:只提供访问者需要的方法。例如开发一个图书管理系统,其中有一个查询接口,其类图如下:
image.png
其中定义了多种查询方法,以及混合查询complexSearch。

程序写好,投产上线后,某天发现系统速度非常慢,最终确定是访问接口中的complexSearch(Map map)方法并发量过大,导致应用服务器性能下降。继续跟踪,发现这些查询都是从公网上发起的。进一步分析:提供给公网的查询接口和提供给系统内管理人员的接口是相同的,都是IBookSearcher接口,但权限不同。系统管理人员可以通过接口的complexSearch方法查询到所有的书籍,而公网的这个方法是被限制的,不返回任何值。在设计时通过口头约束,这个方法是不可被调用的,但是由于公网项目组的疏忽,这个方法公布了出去,虽然不能返回结果,但是还是引起了应用服务器的性能巨慢的情况发生,这就是 一个臃肿接口引起性能故障的案例

对系统进行改进,需要对进口进行重构,将IBookSearcher拆分为两个接口,分别为两个模块提供 定制服务 。修改后的类图如下所示:
image.png
提供给管理人员的实现类同时实现ISimpleBookSearcher和IComplexBookSearcher两个接口,原程序不做任何改变,而提供给公网的接口变为ISimpleBookSearcher,只允许进行简单查询,即单独为其定制服务,减少可能引起的风险。

接口设计是有限度的

接口的设计粒度越小,系统灵活性越高,但也会带来结构复杂化的问题,导致开发难度增加,可维护性降低,因此接口设计要适度。通常根据经验和常识判断。


最佳实践

接口隔离原则是对接口的定义,也是对类的定义,接口和类尽量使用原子接口或原子类来组装。这个 原子 如何划分是设计模式中的一大难题。在实践中,可根据以下几个规则进行衡量:

  • 一个接口只服务于一个子模块或业务逻辑;
  • 通过业务逻辑压缩接口中的public方法,时常回顾接口;
  • 尽量修改已被污染的接口,对变更风险较大的可采用 适配器模式 转化处理;
  • 深入了解业务逻辑,根据项目环境及设计接口。