第六章:接口隔离原则——接口要小而专
接口隔离原则,英文叫 Interface Segregation Principle,简称 ISP。它是 SOLID 五大原则之一。说白了就是一句话:客户端不应该被迫依赖它不需要的接口。
我刚开始接触这个原则时,觉得它不就是「接口要小一点」嘛,有啥好讲的?直到我在一个老项目里接手了一个超级接口,才真正体会到什么叫「胖接口的噩梦」。嗯,今天我们就来好好聊聊这个。
胖接口到底有多「胖」?
先看一个反面教材。假设我们有一个多功能打印机接口:
public interface MultiFunctionPrinter {
void print(String content);
void scan(String path);
void fax(String number);
void copy(String source, String target);
void staple(); // 装订
void duplexPrint(String content); // 双面打印
}
这个接口看起来挺全的,对吧?但问题来了——不是所有打印机都支持所有功能。
比如一个简单的喷墨打印机,它只能打印,不支持扫描、传真、装订。但你强迫它实现这些方法,怎么办?
public class SimplePrinter implements MultiFunctionPrinter {
@Override
public void print(String content) {
System.out.println("打印:" + content);
}
@Override
public void scan(String path) {
throw new UnsupportedOperationException("不支持扫描");
}
@Override
public void fax(String number) {
throw new UnsupportedOperationException("不支持传真");
}
// ... 其他方法全部抛异常
}
我在项目中见过这种代码,真的。一个接口十几个方法,实现类里一半都是 throw new UnsupportedOperationException()。你想想看,调用方拿到这个对象,根本不知道哪些方法能用,哪些不能用。只能靠猜,或者看文档——但文档往往过时了。
- 强迫实现不需要的行为:子类被迫实现一堆无用方法,代码里全是异常或空实现
- 接口变更影响面大:你加一个方法,所有实现类都要改,哪怕它们根本不需要
- 调用方困惑:客户端不知道哪些方法可以安全调用,容易引发运行时异常
接口隔离原则的核心思想
接口隔离原则告诉我们:接口要小而专,一个接口只服务于一个特定的客户端或一组紧密相关的行为。
怎么理解?我个人的习惯是:接口的方法数量控制在 3-5 个以内。如果一个接口的方法超过 7 个,我就会怀疑它是不是太「胖」了。
但注意,不是说方法少就一定好。关键是内聚性——接口里的方法应该属于同一个职责范畴。
实战:拆分多功能接口
好,我们拿上面的多功能打印机来开刀。怎么拆?
先分析一下,这些功能其实可以分成几个独立的职责:
- 打印功能:print, duplexPrint
- 扫描功能:scan
- 传真功能:fax
- 复印功能:copy
- 装订功能:staple
每个职责拆成一个独立的接口:
public interface Printer {
void print(String content);
}
public interface Scanner {
void scan(String path);
}
public interface Fax {
void fax(String number);
}
public interface Copier {
void copy(String source, String target);
}
public interface Stapler {
void staple();
}
public interface DuplexPrinter {
void duplexPrint(String content);
}
现在,一个简单的打印机只需要实现它需要的接口:
public class SimplePrinter implements Printer {
@Override
public void print(String content) {
System.out.println("打印:" + content);
}
}
一个高端多功能一体机可以实现多个接口:
public class HighEndPrinter implements Printer, Scanner, Fax, Copier, Stapler, DuplexPrinter {
@Override
public void print(String content) {
System.out.println("高速打印:" + content);
}
@Override
public void scan(String path) {
System.out.println("高清扫描到:" + path);
}
@Override
public void fax(String number) {
System.out.println("发送传真至:" + number);
}
@Override
public void copy(String source, String target) {
System.out.println("复印:" + source + " -> " + target);
}
@Override
public void staple() {
System.out.println("装订完成");
}
@Override
public void duplexPrint(String content) {
System.out.println("双面打印:" + content);
}
}
你看,这样每个实现类只实现它真正需要的接口。调用方也清楚——你拿到一个 Printer 类型的引用,就知道它肯定能打印,不会抛异常。
接口隔离 vs 单一职责
有人会问:接口隔离原则和单一职责原则(SRP)有什么区别?
嗯,这个问题很好。我简单说一下:
- 单一职责原则:关注的是类的职责,一个类只应该有一个引起它变化的原因
- 接口隔离原则:关注的是接口的设计,接口应该小而专,客户端不应该依赖它不需要的方法
说白了,SRP 是「类要专一」,ISP 是「接口要小巧」。两者相辅相成,但侧重点不同。
如何判断接口是否「胖」了?
我总结了几个判断标准,你可以对照着检查:
| 判断标准 | 说明 |
|---|---|
| 方法数量 | 超过 7 个方法,大概率需要拆分 |
| 实现类中是否有空实现或抛异常 | 如果有,说明接口太胖了 |
| 调用方是否只用了部分方法 | 如果调用方只用了 20% 的方法,接口可能太胖 |
| 接口变更时是否影响多个不相关的实现类 | 如果加一个方法导致多个无关类修改,接口太胖 |
接口隔离的 SVG 结构图
下面这张图展示了胖接口拆分前后的对比:
避坑指南
我曾经在一个项目中,把接口拆得太细了。一个订单系统,我拆了十几个接口,每个接口就一两个方法。结果呢?类爆炸了,调用方要同时依赖五六个接口,反而更复杂。
所以,接口隔离不是「越细越好」。我建议你把握一个度:
- 接口的粒度应该与客户端的视角一致。如果客户端总是同时使用打印和扫描,那把它们放在一个接口里也没问题
- 不要为了隔离而隔离。如果一个接口目前只有一个实现类,而且短期内不会有变化,那保持现状就好
- 重构比设计更重要。我很少一开始就把接口设计得完美无缺。通常是先写一个接口,等发现它变「胖」了,再拆分。代码是活的,允许它慢慢变好
- 胖接口的典型症状:实现类抛异常、调用方困惑、变更影响面大
- 接口隔离的核心:小而专,一个接口只服务一个客户端
- 拆分方法:按职责拆成多个小接口,实现类按需实现
- 注意度:不是越细越好,要结合实际情况
好了,接口隔离原则就聊到这里。记住一句话:接口是契约,契约越简单,越不容易出错。
公众号:蓝海资料掘金营,微信 deep3321