书接前文,在了解了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的功能的接口