跳到主要内容

接口隔离原则

什么是接口隔离原则

接口隔离原则 (Interface Segregation Principle, ISP) 是 SOLID 原则之一,由 Robert Martin 提出。其核心思想是:

Clients should not be forced to depend upon interfaces that they do not use.

客户端不应该被强迫依赖它不需要的接口。

这里的"客户端"可以理解为接口的调用者或者使用者。

理解ISP的关键在于理解"接口"的含义,它可以有不同的解读。

"接口"的三种理解与应用

1. 理解为一组 API 接口集合

当"接口"指代一组API集合时(如微服务接口、类库接口),ISP意味着:如果一个接口集合中的部分接口只被部分调用者使用,那么应该将这部分接口隔离出来,形成更小的接口集合,单独提供给需要的调用者。

目的:避免调用者依赖它们不需要的接口,减少耦合,提高安全性(如防止误用敏感接口)。

示例

假设有一个UserService提供用户注册、登录、获取信息等通用功能。

public interface UserService {
boolean register(String cellphone, String password);
boolean login(String cellphone, String password);
UserInfo getUserInfoById(long id);
UserInfo getUserInfoByCellphone(String cellphone);
}

现在需要一个删除用户的接口,但这个接口非常敏感,只希望给后台管理系统使用。

不好的做法:直接在UserService中添加deleteUserById()。 这会导致所有依赖UserService的系统都能看到并可能误用删除接口。

符合ISP的做法:创建一个新的、职责更单一的接口RestrictedUserService,专门放置敏感操作。

public interface RestrictedUserService {
boolean deleteUserByCellphone(String cellphone);
boolean deleteUserById(long id);
}

// 实现类可以同时实现多个接口
public class UserServiceImpl implements UserService, RestrictedUserService {
// ... 实现所有接口方法 ...
}

后台管理系统依赖 RestrictedUserService,其他系统仅依赖 UserService

2. 理解为单个 API 接口或函数

当"接口"指代单个API或函数时,ISP意味着:函数的设计要功能单一,不要在一个函数中耦合多个不同的功能逻辑。如果调用者只需要函数的部分功能,那么这个函数可能职责不够单一,应该考虑拆分。

目的:提高函数的内聚性、复用性,避免调用者执行不必要的操作,提升性能。

示例

一个统计函数count()计算并返回一个包含多种统计指标的对象。

public class Statistics {
private Long max;
private Long min;
private Long average;
private Long sum;
// ... 省略其他指标和方法 ...
}

public Statistics count(Collection<Long> dataSet) {
Statistics statistics = new Statistics();
// 计算所有指标:max, min, average, sum ...
return statistics;
}

问题:如果某个调用者只需要maxmin,但count()却计算了所有指标,造成了性能浪费。

符合ISP的做法:将count()拆分成多个功能更单一的函数。

public Long max(Collection<Long> dataSet) { /* ... */ }
public Long min(Collection<Long> dataSet) { /* ... */ }
public Long average(Collection<Long> dataSet) { /* ... */ }
public Long sum(Collection<Long> dataSet) { /* ... */ }

调用者按需调用即可。

注意:判断职责是否单一需要结合场景。如果大部分调用者都需要所有统计信息,那么count()函数可能是合理的。

3. 理解为 OOP 中的接口概念

当"接口"指代面向对象编程语言中的interface时,ISP意味着:接口的设计要尽量小而专,包含尽可能少的方法。不要让实现类依赖它们不需要的方法,也不要强迫调用者依赖它们不需要的方法。

目的:提高接口的内聚性,降低实现类和调用者之间的耦合度,提高灵活性和可复用性。

示例

假设有RedisConfig, KafkaConfig, MysqlConfig。需要支持部分配置的热更新(update())和部分配置的信息查看(output()/outputInPlainText())。

不好的做法:定义一个大而全的Config接口。

public interface Config {
void update();
String outputInPlainText();
Map<String, String> output();
}

// 所有Config类都需要实现这三个方法,即使它们不需要
public class KafkaConfig implements Config { /* update需要,output不需要但必须实现 */ }
public class MysqlConfig implements Config { /* update不需要但必须实现,output需要 */ }

问题

  1. 实现类被迫实现不必要的方法(如KafkaConfig的output)。
  2. 调用者(如热更新调度器ScheduledUpdater)被迫依赖整个Config接口,即使它只关心update()
  3. 接口不灵活,添加新方法会影响所有实现类。

符合ISP的做法:定义两个小而专的接口UpdaterViewer

public interface Updater {
void update();
}

public interface Viewer {
String outputInPlainText();
Map<String, String> output();
}

// 按需实现接口
public class RedisConfig implements Updater, Viewer { /* 实现所有 */ }
public class KafkaConfig implements Updater { /* 只实现 update */ }
public class MysqlConfig implements Viewer { /* 只实现 output */ }

// 调用者只依赖需要的接口
public class ScheduledUpdater {
private Updater updater;
public ScheduledUpdater(Updater updater, ...) { /* ... */ }
public void run() { updater.update(); }
}

public class SimpleHttpServer {
private Map<String, List<Viewer>> viewers = ...;
public void addViewer(String url, Viewer viewer) { /* ... */ }
public void run() { /* 调用 viewer.output... */ }
}

好处

  1. 实现类只实现需要的方法。
  2. 调用者只依赖需要的接口,耦合度低。
  3. 接口更灵活、可复用(例如,Viewer接口可以被其他需要展示信息的类如Metrics复用)。

ISP 与 SRP 的区别

  • 单一职责原则 (SRP):主要关注模块、类、接口自身的职责是否单一。一个类或模块应该有且只有一个引起它变化的原因。
  • 接口隔离原则 (ISP):更侧重于接口的设计,并从 调用者 的角度来审视接口。它提供了一个判断接口职责是否单一的标准:如果调用者只使用了接口的部分方法或功能,那么该接口可能设计得不够单一,违反了ISP。

可以认为ISP是SRP在接口设计层面的一个具体体现和指导原则。

总结

接口隔离原则的核心是避免"胖接口"(Fat Interface),鼓励使用多个小而专的接口代替一个大而全的接口。遵循ISP可以带来以下好处:

  • 降低耦合:调用者和实现类都只依赖于它们真正需要的部分。
  • 提高内聚:接口功能更单一、更专注。
  • 提升可维护性:修改一个接口的影响范围更小。
  • 增强灵活性和复用性:小接口更容易被组合和复用。

在设计接口时,应始终考虑调用者的需求,保持接口的精简和专注。