接口隔离原则(ISP)¶
一句话
调用方不应该被迫依赖它用不到的方法。
Interface Segregation Principle 的核心是:接口应该按使用者的需求来划分,而不是按实现者的方便来堆砌。一个"大而全"的接口,会让实现类背负无关的方法,让调用方暴露在无关的变化之中。
为什么需要 ISP¶
胖接口带来三类问题:
- 实现负担:实现类被迫写空方法或抛
UnsupportedOperationException; - 编译期耦合:调用方依赖了接口,就依赖了接口上的所有方法签名,接口任何变化都会触发重编译;
- 语义污染:调用方从接口上看不出自己真正需要什么,也难以 mock 测试。
ISP 与 单一职责原则 经常被混为一谈:SRP 关注类的变化原因,ISP 关注接口对调用方的承诺范围。一个类可以同时实现多个小接口,从而既保持职责内聚,又避免让某个调用方看到全部能力。
反例:万能 Worker 接口¶
public interface Worker {
void work();
void eat();
void sleep();
void attendMeeting();
}
public class RobotWorker implements Worker {
@Override public void work() { /* 正常工作 */ }
@Override public void eat() { throw new UnsupportedOperationException(); }
@Override public void sleep() { throw new UnsupportedOperationException(); }
@Override public void attendMeeting() { throw new UnsupportedOperationException(); }
}
RobotWorker 被迫实现了三个无意义的方法。更隐蔽的问题是:任何只想调用 work() 的客户端,也被迫依赖了 eat/sleep/attendMeeting 的存在。
重构:按角色拆分接口¶
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Sleepable {
void sleep();
}
public interface MeetingAttendee {
void attendMeeting();
}
public class HumanWorker implements Workable, Eatable, Sleepable, MeetingAttendee {
@Override public void work() { /* ... */ }
@Override public void eat() { /* ... */ }
@Override public void sleep() { /* ... */ }
@Override public void attendMeeting() { /* ... */ }
}
public class RobotWorker implements Workable {
@Override public void work() { /* ... */ }
}
客户端只依赖自己需要的角色:
public class AssemblyLine {
private final List<Workable> workers;
public AssemblyLine(List<Workable> workers) {
this.workers = workers;
}
public void start() {
workers.forEach(Workable::work);
}
}
AssemblyLine 不需要知道工人会不会吃饭,也不会因为 Eatable 接口变化而重新编译。
接口设计建议¶
让接口从调用方出发
- 角色接口:以调用方的需求命名(
Workable、PaymentProcessor),而不是以实体命名(UserService里塞进几十个方法)。 - 小接口 + 组合:一个类实现多个小接口,比让所有调用方共享一个大接口更好。
- 只在需要处声明:入参类型尽量窄。
save(List<? extends Item>)比save(MyBigService)更灵活。 - 默认方法不是隔离手段:给胖接口加
default方法只能减轻实现负担,不能减少调用方对接口的依赖。
常见误区¶
误区一:接口方法越少越好。 不是。接口应当围绕一个角色内聚。把 work() 和 workOvertime() 拆成两个接口,如果它们总是一起被调用,反而增加了使用成本。
误区二:一个实现类只应实现一个接口。 不是。ArrayList 实现了 List、RandomAccess、Cloneable、Serializable 等多个接口,每个接口服务于不同调用方,这是合理的。
误区三:拆分接口等于拆分实现。 不是。接口可以拆得很细,实现类仍可以是一个,只要它同时实现这些接口。ISP 约束的是依赖面,不是代码组织。
一个典型场景:服务接口膨胀¶
// 反例:随着需求增长不断加方法
public interface UserService {
User findById(long id);
List<User> findAll();
void register(User user);
void updateProfile(User user);
void resetPassword(long id);
void exportToExcel(OutputStream out);
void sendMarketingEmail(long id);
}
后台管理、注册流程、报表导出三类调用方共享同一个接口。更好的划分:
public interface UserQueryService {
User findById(long id);
List<User> findAll();
}
public interface UserCommandService {
void register(User user);
void updateProfile(User user);
void resetPassword(long id);
}
public interface UserExportService {
void exportToExcel(OutputStream out);
}
与其他原则的关系¶
- 与 单一职责原则:SRP 约束类,ISP 约束接口;二者共同降低耦合。
- 与 里氏替换原则:接口越窄,实现类越容易完整兑现契约。
- 与 依赖倒置原则:依赖的抽象应当恰好是调用方需要的那个抽象。
- 与 适配器模式:当无法修改胖接口时,适配器可以把宽接口收窄成调用方需要的窄接口。
检查清单¶
- 是否有实现类被迫写空方法或抛
UnsupportedOperationException? - 接口名是否以实体为中心,而不是以调用方的角色为中心?
- 调用方是否依赖了它根本不调用的方法?
- 接口变化时,是否会导致无关的调用方重新编译或修改?
- 拆出的接口是否围绕同一角色内聚,而不是机械地"一方法一接口"?
最后更新: 2026-10-23 19:51:40
创建日期: 2026-10-23 19:51:40
创建日期: 2026-10-23 19:51:40
广告
人要恰饭的嘛🤑🤑