一. 接口隔离原则的定义
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.
类间的依赖关系应建立在最小的接口上。
也就是说: 接口尽量细化,接口中的方法尽量少
二. 接口隔离原则和单一职责原则
从功能上来看,接口隔离原则和单一职责原则都是为了提高类的内聚, 降低类之间的耦合, 体现了封装的思想。但二者还是有区别的。
(1)从原则约束来看: 接口隔离原则更关注的是接口依赖程度的隔离;而单一职责原则更加注重的是接口职责的划分。
(2)从接口的细化程度来看: 单一职责原则对接口的划分更加精细,而接口隔离原则注重的是相同功能的接口的隔离。接口隔离里面的最小接口有时可以是多个单一职责的公共接口。
(3)单一职责原则更加偏向对业务的约束: 接口隔离原则更加偏向设计架构的约束。这个应该好理解,职责是根据业务功能来划分的,所以单一原则更加偏向业务;而接口隔离更多是为了“高内聚”,偏向架构的设计。
三. 接口隔离原则的优点
接口隔离原则是为了约束接口、降低类对接口的依赖性,遵循接口隔离原则有以下 5 个优点。
- 将臃肿庞大的接口分解为多个粒度小的接口,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
- 接口隔离提高了系统的内聚性,减少了对外交互,降低了系统的耦合性。
- 如果接口的粒度大小定义合理,能够保证系统的稳定性;然而,如果定义过小,则会造成接口数量过多,使设计复杂化;如果定义太大,灵活性降低,无法提供定制服务,给整体项目带来无法预料的风险。
- 使用多个专门的接口能够体现对象的层次,因为可以通过接口的继承,实现对总接口的定义。
- 能减少项目工程中的代码冗余。过大的大接口里面通常放置许多不用的方法,当实现这个接口的时候,被迫设计冗余的代码。
四. 接口隔离原则的实现方法
在具体应用接口隔离原则时,应该根据以下几个规则来衡量。
1)接口要尽量小
不能出现Fat Interface;但是要有限度,首先不能违反单一职责原则(不能一个接口对应半个职责)。
2)接口要高内聚
在接口中尽量少公布public方法。
接口是对外的承诺,承诺越少对系统的开发越有利。
3)定制服务
只提供访问者需要的方法。例如,为管理员提供IComplexSearcher接口,为公网提供ISimpleSearcher接口。
4)接口的设计是有限度的
了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同, 需要深入了解业务逻辑。
五. 接口隔离原则的建议
- 一个接口只服务于一个子模块或业务逻辑;
- 通过业务逻辑压缩接口中的public方法;
- 已被污染了的接口,尽量去修改;若变更的风险较大,则采用适配器模式转化处理;
- 拒绝盲从
五. 案例分析
下面以学生成绩管理为例来说明接口隔离原则:
分析:学生成绩管理程序一般包含查询成绩、新增成绩、删除成绩、修改成绩、计算总分、计算平均分、打印成绩信息等功能,通常我们会怎么做呢?
一: 最初的设计
通常我们设计接口的方式如下:
public interface IStudentScore { // 查询成绩 public void queryScore(); // 修改成绩 public void updateScore(); // 添加成绩 public void saveScore(); // 删除成绩 public void delete(); // 计算总分 public double sum(); // 计算平均分 public double avg(); // 打印成绩单 public void printScore(); }
我们会吧所有的功能都放在一个接口里面. 这会产生什么样的问题呢?
首先, 接口的方法很多, 不利于扩展. 比如: 学生只有查看成绩,打印成绩单的权限, 没有增删改的权限; 老师拥有所有的权限.
查询成绩单:
package com.lxl.www.designPatterns.sixPrinciple.interfaceSegregationPrinciple.score; public class QueryScore implements IStudentScore{ @Override public void queryScore() { // 查询成绩 } @Override public void updateScore() { // 没有权限 } @Override public void saveScore() { // 没有权限 } @Override public void delete() { // 没有权限 } @Override public double sum() { // 没有权限 return 0; } @Override public double avg() { // 没有权限 return 0; } @Override public void printScore() { //打印成绩单 } }
操作成绩单
package com.lxl.www.designPatterns.sixPrinciple.interfaceSegregationPrinciple.score; public class Operate implements IStudentScore{ @Override public void queryScore() { } @Override public void updateScore() { } @Override public void saveScore() { } @Override public void delete() { } @Override public double sum() { return 0; } @Override public double avg() { return 0; } @Override public void printScore() { } }
可以看出问题. 查询成绩单, 我们只会用到两个方法, 可是因为实现了接口, 不得不重写所有的方法.
如果这时候增加需求--发送给家长, 只有老师才有这个权限, 学生没有这个权限. 可是, 在接口中增加一个抽象方法以后, 所有的实现类都要重写这个方法. 这就违背了开闭原则.
2. 使用接口隔离原则的设计
采用接口隔离原则设计的接口, UML图如下:
public interface IQueryScore { // 查询成绩 public void queryScore(); // 打印成绩单 public void printScore(); } public interface IOperateScore { // 修改成绩 public void updateScore(); // 添加成绩 public void saveScore(); // 删除成绩 public void delete(); // 计算总分 public double sum(); // 计算平均分 public double avg(); } public class StudentOperate implements IQueryScore{ @Override public void queryScore() { // 查询成绩 } @Override public void printScore() { //打印成绩单 } } public class TeacherOperate implements IQueryScore, IOperateScore{ @Override public void queryScore() { } @Override public void updateScore() { } @Override public void saveScore() { } @Override public void delete() { } @Override public double sum() { return 0; } @Override public double avg() { return 0; } @Override public void printScore() { } }
我们将原来的一个接口进行了接口拆分. 分为查询接口和操作接口. 这样学生端就不需要重写和他不相关的接口了.
如果将这些功能全部放到一个接口中显然不太合理,正确的做法是将它们分别放在输入模块、统计模块和打印模块等 3 个模块中,其类图如图 1 所示