接口隔离原则
接口隔离原则的定义
在讲接口隔离原则之前,先明确一下我们的主角——接口。
接口分为两种:
- 实例接口(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个方法都放在一个接口中,并且提供给多个模块访问,各个模块按照规定的权限来访问,在系统外通过文档约束“不使用的方法不要访问”,按照单一职责原则是允许的,按照接口隔离原则是不允许的,因为它要求“尽量使用多个专门的接口”。
专门的接口指什么?就是指提供给每个模块的都应该是单一接口,提供给几个模块就应该有几个接口,而不是建立一个庞大的臃肿的接口,容纳所有的客户端访问。
美女何其多,观点各不同
我们举例来说明接口隔离原则到底对我们提出了什么要求。
现在男生对小姑娘的称呼,使用频率最高的应该是“美女”了吧,你在大街上叫一声:“嗨,美女!”估计10个有8个回头,其中包括那位著名的如花。
美女的标准各不相同,首先就需要定义一下什么是美女:首先要面貌好看,其次是身材要窈窕,然后要有气质,当然了,这三者各人的排列顺序不一样,总之要成为一名美女就必须具体:面貌、身材和气质,我们用类图体现一下星探(当然,你也可以把自己想象成星探)找美女的过程,如图所示。
定义了一个 IPrettyGirl 接口,声明所有的美女都应该有 goodLooking、niceFigure 和 greatTemperament,然后又定义了一个抽象类 AbstractSearcher,其作用就是搜索美女并显示其信息,只要美女都按照这个规范定义,Searcher(星探)就轻松多了,美女类的实现代码如下:
/**
* 美女类
*/
public interface IPrettyGirl {
//要有姣好的面孔
public void goodLooking();
//要有好身材
public void niceFigure();
//要有气质
public void greatTemperament();
}
美女的标准定义完毕,具体的美女实现类代码如下:
/**
* 美女实现类
*/
public class PrettyGirl implements IPrettyGirl {
private String name;
//美女都有名字
public PrettyGirl(String name) {
this.name = name;
}
//脸蛋漂亮
@Override
public void goodLooking() {
System.out.println(this.name + "---脸蛋很漂亮!");
}
//身材要好
@Override
public void niceFigure() {
System.out.println(this.name + "---身材非常棒!");
}
//气质要好
@Override
public void greatTemperament() {
System.out.println(this.name + "---气质非常好!");
}
}
通过三个方法,把对美女的要求都定义出来了,按照这个标准,如花姑娘被排除在美女标准之外了。 有美女,就有搜索美女的星探,其具体实现代码如下:
/**
* 星探抽象类
*/
public abstract class AbstractSearcher {
protected IPrettyGirl prettyGirl;
public AbstractSearcher(IPrettyGirl prettyGirl) {
this.prettyGirl = prettyGirl;
}
//搜索美女,列出美女信息
public abstract void show();
}
星探的实现类就比较简单了,其源代码如下:
public class Searcher extends AbstractSearcher {
public Searcher(IPrettyGirl prettyGirl) {
super(prettyGirl);
}
//展示美女的信息
@Override
public void show() {
System.out.println("--------美女的信息如下:--------");
//展示面容
super.prettyGirl.goodLooking();
//展示身材
super.prettyGirl.niceFigure();
//展示气质
super.prettyGirl.greatTemperament();
}
}
场景中的两个角色美女和星探都已经出现了,需要写一个场景类来串联起各个角色,场景类的实现代码如下:
/**
* 场景类
*/
public class Client {
//搜索并展示美女信息
public static void main(String[] args) {
//定义一个美女
IPrettyGirl yanYan = new PrettyGirl("嫣嫣");
AbstractSearcher searcher = new Searcher(yanYan);
searcher.show();
}
}
星探搜索美女的运行结果如下:
--------美女的信息如下:--------
嫣嫣---脸蛋很漂亮!
嫣嫣---身材非常棒!
嫣嫣---气质非常好!
星探寻找美女的程序开发完毕了,运行结果也正确。
我们回头来想想这个程序有没有问题,思考一下 IPrettyGirl 这个接口,这个接口是否做到了最优化设计?答案是没有,还可以对接口进行优化。
我们的审美观点都在改变,美女的定义也在变化。唐朝的杨贵妃如果活到现在这个年代非羞愧而死不可,为什么?胖呀!
但是胖并不影响她入选中国四大美女,说明当时的审美观与现在是有差异的。
当然,随着时代的发展我们的审美观也在变化,当你发现有一个女孩,脸蛋不怎么样,身材也一般般,但是气质非常好,我相信大部分人都会把这样的女孩叫美女,审美素质提升了,就产生了气质型美女,但是我们的接口却定义了美女必须是三者都具备,按照这个标准,气质型美女就不能算美女,那怎么办?
可能你要说了,我重新扩展一个美女类,只实现 greatTemperament 方法,其他两个方法置空,什么都不写,不就可以了吗?
聪明,但是行不通!为什么呢?星探 AbstractSearcher 依赖的是 IPrettyGirl 接口,它有三个方法,你只实现了两个方法,星探的方法是不是要修改?
我们上面的程序打印出来的信息少了两条,还让星探怎么去辨别是不是美女呢?
分析到这里,我们发现接口 IPrettyGirl 的设计是有缺陷的,过于庞大了,容纳了一些可变的因素,根据接口隔离原则,星探 AbstractSearcher 应该依赖于具有部分特质的女孩子,而我们却把这些特质都封装了起来,放到了一个接口中,封装过度了!问题找到了,我们重新设计一下类图,修改后的类图如下:
把原 IPrettyGirl 接口拆分为两个接口,一种是外形美的美女 IGoodBodyGirl,这类美女的特点就是脸蛋和身材极棒,超一流,但是没有审美素质,比如随地吐痰,文化程度比较低;
另外一种是气质美的美女 IGreatTemperamentGirl,谈吐和修养都非常高。
我们把一个比较臃肿的接口拆分成了两个专门的接口,灵活性提高了,可维护性也增加了,不管以后是要外形美的美女还是气质美的美女都可以轻松地通过 PrettyGirl 定义。
两种类型的美女定义代码如下:
public interface IGoodBodyGirl {
//要有姣好的面孔
public void goodLooking();
//要有好身材
public void niceFigure();
}
public interface IGreatTemperamentGirl {
//要有气质
public void greatTemperament();
}
按照脸蛋、身材、气质都具备才算美女,实现类实现两个接口,代码如下:
public class PrettyGirl implements IGoodBodyGirl, IGreatTemperamentGirl {
private String name;
//美女都有名字
public PrettyGirl(String name) {
this.name = name;
}
//脸蛋漂亮
@Override
public void goodLooking() {
System.out.println(this.name + "---脸蛋很漂亮!");
}
//身材要好
@Override
public void niceFigure() {
System.out.println(this.name + "---身材非常棒!");
}
//气质要好
@Override
public void greatTemperament() {
System.out.println(this.name + "---气质非常好!");
}
}
通过这样的重构以后,不管以后是要气质美女还是要外形美女,都可以保持接口的稳定。
当然,你可能要说了,以后可能审美观点再发生改变,只有脸蛋好看就是美女,那这个 IGoodBody 接口还是要修改的呀,确实是,但是设计是有限度的,不能无限地考虑未来的变更情况,否则就会陷入设计的泥潭中而不能自拔。
以上把一个臃肿的接口变更为两个独立的接口所依赖的原则就是接口隔离原则,让星探 AbstractSearcher 依赖两个专用的接口比依赖一个综合的接口要灵活。
接口是我们设计时对外提供的契约,通过分散定义多个接口,可以预防未来变更的扩散,提高系统的灵活性和可维护性。
保证接口的纯洁性
- 接口隔离原则是对接口进行规范约束,其包含以下4层含义:
- 接口要尽量小
- 这是接口隔离原则的核心定义,不出现臃肿的接口(Fat Interface),但是“小”是有限度的,首先就是不能违反单一职责原则,什么意思呢?
我们在单一职责原则中提到一个 IPhone 的例子,在这里,我们使用单一职责原则把两个职责分解到两个接口中,类图如图所示: