书接前文,在了解了SRP、OCP、LSP之后,再来看看ISP接口隔离原则的定义和真实的内涵
理解接口隔离原则
接口隔离原则的英文翻译是Interface Segregation Principle,缩写为ISP,客户端不应该被强迫依赖它不需要的接口。其中的客户端,可以理解为接口的调用者或者使用者,这里的接口不仅指Java里的接口类,实际上它有三种含义: 一组 API 接口或方法集合; 单个 API 接口或方法; OOP 中的接口概念
一组 API 接口或方法集合
如果把接口当做一组方法集合,那么接口隔离原则就是,仅暴露给调用者他关心的接口。在设计微服务或类库时,我们经常需要提供一组操作给用户使用,例如用户的相关信息可以存储为一组方法集合:
public interface UserService {
boolean register(String cellphone, String password);
boolean login(String cellphone, String password);
UserInfo getUserInfoById(long id);
UserInfo getUserInfoByCellphone(String cellphone);
}
public class UserServiceImpl implements UserService {
//...
}
此时我们需要增加删除用户这样的操作,这样的操作较为危险,不能提供给普通用户操作,同时普通用户也不需要操作,这个功能是给管理员设计的,所以为了避免暴露这样的方法给普通用户,我们对接口进行隔离:
// 普通用户操作行为
public interface UserService {
boolean register(String cellphone, String password);
boolean login(String cellphone, String password);
UserInfo getUserInfoById(long id);
UserInfo getUserInfoByCellphone(String cellphone);
}
// 超管操作行为
public interface RestrictedUserService {
boolean deleteUserByCellphone(String cellphone);
boolean deleteUserById(long id);
}
public class UserServiceImpl implements UserService, RestrictedUserService {
// ...省略实现代码...
}
如果部分接口只被部分调用者使用,那我们就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖这部分不会被用到的接口
单个 API 接口或方法
如果把接口理解为单个方法,那接口隔离原则就可以理解为:函数的设计要功能单一,不要将多个不同的功能逻辑在一个函数中实现
以一个统计方法为例:
public class Statistics {
private Long max;
private Long min;
private Long average;
private Long sum;
private Long percentile99;
private Long percentile999;
//...省略constructor/getter/setter等方法...
}
public Statistics count(Collection<Long> dataSet) {
Statistics statistics = new Statistics();
//...省略计算逻辑...
return statistics;
}
在上面的代码中,count() 函数的功能不够单一,包含很多不同的统计功能,比如,求最大值、最小值、平均值等等。按照接口隔离原则,我们应该把 count() 函数拆成几个更小粒度的函数,每个函数负责一个独立的统计功能
public Long max(Collection<Long> dataSet) { //... }
public Long min(Collection<Long> dataSet) { //... }
public Long average(Colletion<Long> dataSet) { //... }
// ...省略其他统计函数...
这样的好处是如果我们常用的统计是sum,那么其它的方法都需要耗费无意义的算力,不过判定功能是否单一,除了很强的主观性,还需要结合具体的场景。如果我们确实每次都需要所有的统计逻辑,那么这个方法也不需要拆分。看使用者如何去看待了。
OOP 中的接口概念
如果把“接口”理解为 OOP 中的接口,也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数
热更新接口
public interface Updater {
void update();
}
监控接口
public interface Viewer {
String outputInPlainText();
Map<String, String> output();
}
Redis,Kafka,Mysql配置实现
// Redis支持热更新和监控
public class RedisConfig implemets Updater, Viewer {
//...省略其他属性和方法...
@Override
public void update() { //... }
@Override
public String outputInPlainText() { //... }
@Override
public Map<String, String> output() { //...}
}
// Kafka只支持热更新
public class KafkaConfig implements Updater {
//...省略其他属性和方法...
@Override
public void update() { //... }
}
// Mysql只支持监控
public class MysqlConfig implements Viewer {
//...省略其他属性和方法...
@Override
public String outputInPlainText() { //... }
@Override
public Map<String, String> output() { //...}
}
热更新管理器,基于接口的方式依赖注入
public class ScheduledUpdater {
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();;
private long initialDelayInSeconds;
private long periodInSeconds;
private Updater updater;
public ScheduleUpdater(Updater updater, long initialDelayInSeconds, long periodInSeconds) {
this.updater = updater;
this.initialDelayInSeconds = initialDelayInSeconds;
this.periodInSeconds = periodInSeconds;
}
public void run() {
executor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
updater.update();
}
}, this.initialDelayInSeconds, this.periodInSeconds, TimeUnit.SECONDS);
}
}
监控管理器,基于接口的方式依赖注入
public class SimpleHttpServer {
private String host;
private int port;
private Map<String, List<Viewer>> viewers = new HashMap<>();
public SimpleHttpServer(String host, int port) {//...}
public void addViewers(String urlDirectory, Viewer viewer) {
if (!viewers.containsKey(urlDirectory)) {
viewers.put(urlDirectory, new ArrayList<Viewer>());
}
this.viewers.get(urlDirectory).add(viewer);
}
public void run() { //... }
}
基于不同的需求使用热更新和监控
public class Application {
ConfigSource configSource = new ZookeeperConfigSource();
public static final RedisConfig redisConfig = new RedisConfig(configSource);
public static final KafkaConfig kafkaConfig = new KakfaConfig(configSource);
public static final MySqlConfig mysqlConfig = new MySqlConfig(configSource);
public static void main(String[] args) {
ScheduledUpdater redisConfigUpdater =
new ScheduledUpdater(redisConfig, 300, 300);
redisConfigUpdater.run();
ScheduledUpdater kafkaConfigUpdater =
new ScheduledUpdater(kafkaConfig, 60, 60);
redisConfigUpdater.run();
SimpleHttpServer simpleHttpServer = new SimpleHttpServer(“127.0.0.1”, 2389);
simpleHttpServer.addViewer("/config", redisConfig);
simpleHttpServer.addViewer("/config", mysqlConfig);
simpleHttpServer.run();
}
}
ISP和SRP的区别
通过拆分方法让代码粒度变细的方式,ISP和SRP有点类似,不过稍微还是有点区别。单一职责原则针对的是模块、类、接口的设计。而接口隔离原则相对于单一职责原则,一方面它更侧重于接口的设计,另一方面它的思考的角度不同。它提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。也就是说ISP的判定更为主观一些。
总结一下
ISP中的接口对于不同功能诉求的使用者来说,可以当做一组 API 接口或方法集合,按照使用者分类来暴露给不同使用者差异化的接口集,不要给使用者它不care的功能;对于单一功能的使用者来说,可以当做一个API接口或方法集合,按照使用者的场景诉求主观判断是否需要拆分,不要给使用者他不care的复杂方法实现;对于一个固定的需求实现而言,可以当做一个OOP的接口去看待,按照需求定义接口,不要让接口的实现类和调用者,依赖它不care的功能的接口