接口隔离原则 Interface Segregation Principle , ISP
接口隔离原则定义
客户端不应该被迫依赖它不需要的接口。其中的“客户端”可以理解为接口的调用者或者使用者。
如何理解接口隔离原则
理解接口隔离原则的关键,就是理解其中的“接口”二字。在接口隔离原则中,“接口”可以理解为下面三种情况:
-
一组API接口集合
当把接口隔离原则的接口理解成一组API接口集合时,可以是某个微服务的接口,也可以是一些类库的接口等,在设计微服务或者类库接口时,如果部分接口只被部分调用者使用,那只需要单独把需要的接口隔离出来,单独提供给需要的调用者,而不是强迫其他调用者也依赖这部分不会被用到的接口。比如用户微服务系统需要给一些其他系统提供注册、登录、获取用户信息等接口,还需要给一些系统需要删除用户的接口。如果我们把所有的接口都放在一起对外提供,那就会存在安全隐患,因为删除操作是一个需要很慎重的操作,应该只限于给有权限的系统调用,如果放在一个接口暴露给所有系统,那就有可能导致一些系统误删用户。所以要么给调用者做接口鉴权,要么通过代码控制,参照接口隔离原则,将删除接口隔离出来,只提供给需要的微服务系统。
public interface UserService{
boolean register(User user);
boolean login(User user);
User getUserInfoById(long id);
boolean deleteUserById(long id);
}
public class UserServiceImpl implements UserService{
//...
}
//**************************修改后***********************************
public interface UserService{
boolean register(User user);
boolean login(User user);
User getUserInfoById(long id);
}
public interface RestrictedUserService{
boolean deleteUserById(long id);
}
public class UserServiceImpl implements UserService, RestrictedUserService{
//...
}
- 单个API接口或函数
当把接口理解为单个API接口或函数时,接口隔离原则说的就是接口函数的设计要功能单一,不要将多个不同的功能逻辑放在一个函数中实现,比如一个系统需要对外提供数据统计接口,如果调用者系统需要的统计数据不一致,有的系统需要max、min、average的统计信息,有的系统只需要average信息,有的只需要sum统计信息,而我们将所有的统计信息都放在一个函数里,等于就是每个调用者系统调用统计信息是,都要将所有的统计数据计算一遍,如果在数据量比较大的时候,就会做很多无用功,影响代码的性能。这里我们就应该将统计函数拆分成细粒度的统计函数,然后调用者系统根据需要调用。
public class statistics{
private long max;
private long min;
private long average;
private long sum;
private long percentile99;
private long percentile999;
}
public Statistics count(Collection<Long> dataSet){
//...
}
//**************************修改后***********************************
public Long max(Collection<Long> dataSet){ // ... }
public Long min(Collection<Long> dataSet){ // ... }
public Long average(Collection<Long> dataSet){ // ... }
public Long sum(Collection<Long> dataSet){ // ... }
// ...
- 面向对象编程中的接口概念
当把接口隔离原则的”接口“理解为面向对象编程中的接口时,比如java中的interface。那接口隔离原则说的就是接口的设计要尽量单一,不要让接口的实现类和调用者依赖不需要的接口函数。
比如我们有一个系统依赖了Kafka,redis 和MySQL,现在需要对这三个组件的配置项实现热更新和监控功能。如果我们将两个功能像如下代码一样设计在一个接口内:
public interface Config{
void update(); //热更新接口
String outputInPlainText(); //配置打印
Map<String,String> output();
}
public class RedisConfig implements Config{
//接口实现
}
public class KafkaConfig implements Config{
//接口实现
}
public class MysqlConfig implements Config{
//接口实现
}
//热更新类
public class SchedulerUpdater{
//... 省略其他属性和方法...
private Config config;
public ScheduleUpdater(Config config,long initialDelayInSeconds,long periodInSeconds){
//...
}
//...
}
//监控类
public class SimpleHttpServer{
private String host;
private int port;
private Map<String,List<Config>> viewers = new HashMap<>();
public SimpleHttpServer(String host, int port){ //... }
public void addViewer(String urlDirectory, Config config){
// ...
}
}
以上Config接口包含两类不相关的接口,如果Kafka配置项只需要热更新,不需要监控查看,但由于Config接口包含所有接口,KafkaConfig不得不同时实现所有接口函数,且如果Config接口有任何改动,所有的子类都需要改动,而如果接口的粒度更小,那改动接口函数,涉及改动的子类就相对较少。同时如果有新的监控需求,由于Config的设计,新的功能类不得不实现一些毫无相关的接口函数。
如果我们将Config接口拆开:
// 配置类定义
public class RedisConfig{
private ConfigSource configSource; // 配置中心(比如zookeeper)
private String address;
private int timeout;
private int maxTotal;
//省略其他配置
public RedisConfig(ConfigSource configSource){
this.configSource = configSource;
}
//省略getter setter
public void update(){
//从configSource加载配置到address/timeout/maxTotal
}
}
public class KafkaConfig{ //... }
public class MysqlConfig{ //... }
//热更新
//ScheduleUpdater类以固定时间频率(periodInSeconds)调用需要热更新的
//RedisConfig、KafkaConfig的update()方法更新配置信息。
public interface Updater{
void update();
}
public class RedisConfig implements Updater{
//...省略其他属性和方法
@Override
public void update(){ // ... }
}
public class KafkaConfig implements Updater{
//...省略其他属性和方法
@Override
public void update(){ // ... }
}
public class MysqlConfig{ // ... }
public class ScheduleUpdater{
private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
private long initalDelayInSeconds;
private long periodInSeconds;
private Updater updater;
public ScheduleUpdater(Updater updater, long initalDelayInSeconds,long periodInSeconds){
this.Updater = updater;
this.initalDelayInSeconds = initalDelayInSeconds;
this.periodInSeconds = periodInSeconds;
}
public void run(){
executor.sheduleAtFixedRate(new Runnable(){
@Override
public void run(){
updater.update();
}
},this.initalDelayInSeconds,this.periodInSeconds,TimeUnit.SECONDS);
}
}
public class Application{
ConfigSource configSource = new ZookeeperConfigSource(...);
public static final RedisConfig redisConfig = new RedisConfig(configSource);
public static final KafkaConfig kafkaConfig = new KafkaConfig(configSource);
public static final MysqlConfig mysqlConfig = new MysqlConfig(configSource);
public static void main(String[] args){
ScheduleUpdater redisConfigUpdater = new ScheduleUpdater(redisConfig,300,300);
redisConfigUpdater.run();
ScheduleUpdater kafkaConfigUpdater = new ScheduleUpdater(kafkaConfig,300,300);
kafkaConfigUpdater.run();
}
}
//配置项监控
//在项目中内嵌一个简单的SimpleHttpServer,当访问/config时,在页面显示系统的配置信息。
//这里只有Mysql和Redis需要显示配置信息,Kafka并不想暴露配置信息
public interface Updater{
void update();
}
public interface Viewer{
String outputInPlainText();
Map<String,String> output();
}
public class RedisConfig implements Updater, Viewer{
//...省略其他属性和方法...
@Override
public void update(){ //... }
@Override
public String outputInPlainText(){ //... }
@Override
public Map<String, String> output(){ //... }
}
public class KafkaConfig implements Updater{
//...省略其他属性和方法
@Override
public void update(){ // ... }
}
public class MysqlConfig implements Viewer{
//...省略其他属性和方法...
@Override
public String outputInPlainText(){ //... }
@Override
public Map<String, String> output(){ //... }
}
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 KafkaConfig(configSource);
public static final MysqlConfig mysqlConfig = new MysqlConfig(configSource);
public static void main(String[] args){
ScheduleUpdater redisConfigUpdater = new ScheduleUpdater(redisConfig,300,300);
redisConfigUpdater.run();
ScheduleUpdater kafkaConfigUpdater = new ScheduleUpdater(kafkaConfig,60,60);
kafkaConfigUpdater.run();
SimpleHttpServer simpleHttpServer = new SimpleHttpServer("127.0.0.1",2323);
simpleHttpServer.addViewer("/config",redisConfig);
simpleHttpServer.addViewer("/config",mysqlConfig);
simpleHttpServer.run();
}
}
以上两个功能单一的接口:Updater和Viewer,隔离了热更新和配置项监控功能,满足接口隔离原则,这种职责单一的接口设计更加灵活,可扩展性好,复用性好。
接口隔离原则和单一职责原则的区别
单一职责原则针对的是模块、类、接口的设计,接口隔离原则相对于单一职责原则,一方面更侧重于接口的设计,另一方面通过调用者如何使用接口间接地提供了一种判断接口职责是否单一的标准。如果调用者只使用了部分接口或接口的部分功能,那接口的设计就不够职责单一。