设计模式之美-课程笔记10-设计原则4-接口隔离

理论四:接口隔离原则有哪三种应用?原则中的“接口”该如何理解?

如何理解”接口隔离原则“

  1. Interface Segregation Principle: ISP. Clients should not be force to depend upon interfaces that they not use. 客户端(调用者、使用者)应该被强迫依赖他不需要的接口。
  2. 关键在于如何理解接口
    • 一组API接口集合
    • 单个API接口或函数
    • OOP中的接口概念

把接口理解为一组API接口集合

  1. 举个例子,微服务用户系统提供了一组跟用户相关的 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 {
      //...
    }
    
  2. 如果现在要实现删除用户的功能,我们可能会想到的是在接口中增加一个方法deleteUserById.

    1. 问题在于,删除用户的权限最好是后台管理系统执行。如果放进去UserService,那所有实现这个接口的类都可以使用这个接口。不加限制的被其他业务系统调用,有可能导致误删用户。

    2. 最好的设计是从架构层面,增加鉴权系统来限制接口的调用。但是目前没有支持的话,我们也可从代码层面去做。

    3. 参照接口隔离原则,调用者不应该强迫依赖他不需要的接口。我们可以将删除接口单独放到另一个接口RestrictedUserService中,然后将这个Service只打包给后台管理系统使用:

      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 {
        // ...省略实现代码...
      }
      
  3. 接口在这里被理解为一组接口集合。在设计微服务或者类库接口的时候,如果部分接口只被部分调用者使用,那我们就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖这部分不会被用到的接口。

把接口理解为单个API接口或函数

  1. 如果将接口理解为一个接口,或者函数。那我们这个原则的理解就是: 函数的设计要功能单一,不要讲多个不同的功能逻辑在一个函数中实现。

  2. 看个例子:

    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,还算了min max等,可以拆成更细的粒度。

    public Long max(Collection<Long> dataSet) { //... }
    public Long min(Collection<Long> dataSet) { //... } 
    public Long average(Colletion<Long> dataSet) { //... }
    // ...省略其他统计函数...
    
  3. 是否功能单一,还是要参照业务场景。

    如果在项目中,对每个统计需求,Statistics 定义的那几个统计信息都有涉及,那 count() 函数的设计就是合理的。相反,如果每个统计需求只涉及 Statistics 罗列的统计信息中一部分,比如,有的只需要用到 max、min、average 这三类统计信息,有的只需要用到 average、sum。而 count() 函数每次都会把所有的统计信息计算一遍,就会做很多无用功,势必影响代码的性能,特别是在需要统计的数据量很大的时候。所以,在这个应用场景下,count() 函数的设计就有点不合理了,我们应该按照第二种设计思路,将其拆分成粒度更细的多个统计函数。

  4. ISP和SRP有点区别的: SPR针对的是类和模块的设计,而ISP更侧重于接口的设计。它提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接的判定。(是否是部分使用)。

把接口理解为OOP中的接口

  1. 举个例子:假设我们的项目中用到了三个外部系统:Redis、MySQL、Kafka。每个系统都对应一系列配置信息,比如地址、端口、访问超时时间等。为了在内存中存储这些配置信息,供项目中的其他模块来使用,我们分别设计实现了三个 Configuration 类:RedisConfig、MysqlConfig、KafkaConfig。具体的代码实现如下所示。这里只给出了 RedisConfig 的代码实现:

    public class RedisConfig {
        private ConfigSource configSource; //配置中心(比如zookeeper)
        private String address;
        private int timeout;
        private int maxTotal;
        //省略其他配置: maxWaitMillis,maxIdle,minIdle...
    
        public RedisConfig(ConfigSource configSource) {
            this.configSource = configSource;
        }
    
        public String getAddress() {
            return this.address;
        }
        //...省略其他get()、init()方法...
    
        public void update() {
          //从configSource加载配置到address/timeout/maxTotal...
        }
    }
    
    public class KafkaConfig { //...省略... }
    public class MysqlConfig { //...省略... }
    
  2. 现在,我们有一个新的功能需求,希望支持 Redis 和 Kafka 配置信息的热更新。所谓“热更新(hot update)”就是,如果在配置中心中更改了配置信息,我们希望在不用重启系统的情况下,能将最新的配置信息加载到内存中(也就是 RedisConfig、KafkaConfig 类中)。但是,因为某些原因,我们并不希望对 MySQL 的配置信息进行热更新。

  3. 为了实现这样一个功能需求,我们设计实现了一个 ScheduledUpdater 类,以固定时间频率(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 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 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);
        kafkaConfigUpdater.run();
      }
    }
    
  4. 刚刚的热更新的需求我们已经搞定了。现在,我们又有了一个新的监控功能需求。通过命令行来查看 Zookeeper 中的配置信息是比较麻烦的。所以,我们希望能有一种更加方便的配置信息查看方式。我们可以在项目中开发一个内嵌的 SimpleHttpServer,输出项目的配置信息到一个固定的 HTTP 地址,比如:http://127.0.0.1:2389/config 。我们只需要在浏览器中输入这个地址,就可以显示出系统的配置信息。不过,出于某些原因,我们只想暴露 MySQL 和 Redis 的配置信息,不想暴露 Kafka 的配置信息。

  5. 我们还需要对上面的代码做进一步改造:

    public interface Updater {
      void update();
    }
    
    public interface Viewer {
      String outputInPlainText();
      Map<String, String> output();
    }
    
    public class RedisConfig implemets 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 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();
        }
    }
    
  6. 在viewer和updater这两个例子中,ScheduledUpdater只依赖Updater,不需要被强迫去依赖viewer。满足接口隔离原则。

  7. 如果不遵循ISP,设计出来的代码可能是:一个大的config接口包含所有方法,所有实现他的类都必须实现所有方法:

    public interface Config {
      void update();
      String outputInPlainText();
      Map<String, String> output();
    }
    
    public class RedisConfig implements Config {
      //...需要实现Config的三个接口update/outputIn.../output
    }
    
    public class KafkaConfig implements Config {
      //...需要实现Config的三个接口update/outputIn.../output
    }
    
    public class MysqlConfig implements Config {
      //...需要实现Config的三个接口update/outputIn.../output
    }
    
    public class ScheduledUpdater {
      //...省略其他属性和方法..
      private Config config;
    
      public ScheduleUpdater(Config config, long initialDelayInSeconds, long periodInSeconds) {
          this.config = config;
          //...
      }
      //...
    }
    
    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) {
        if (!viewers.containsKey(urlDirectory)) {
          viewers.put(urlDirectory, new ArrayList<Config>());
        }
        viewers.get(urlDirectory).add(config);
      }
      
      public void run() { //... }
    }
    
  8. 也能实现,但是对比之前的设计,明显遵循了ISP的代码**扩展性更好,更灵活。**因为Updater和Viewer职责更单一,单一意味着通用、复用性好。比如我们又上了一个Metrics性能统计模块,并希望Metrics也通过SimpleHttpServer显示,此时他就可以使用Viewer接口:

    public class ApiMetrics implements Viewer {//...}
    public class DbMetrics implements Viewer {//...}
    
    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 final ApiMetrics apiMetrics = new ApiMetrics();
        public static final DbMetrics dbMetrics = new DbMetrics();
        
        public static void main(String[] args) {
            SimpleHttpServer simpleHttpServer = new SimpleHttpServer(“127.0.0.1”, 2389);
            simpleHttpServer.addViewer("/config", redisConfig);
            simpleHttpServer.addViewer("/config", mySqlConfig);
            simpleHttpServer.addViewer("/metrics", apiMetrics);
            simpleHttpServer.addViewer("/metrics", dbMetrics);
            simpleHttpServer.run();
        }
    }
    
  9. 第二种设计也有一些无用功,正如我前面说的, 必须要实现所有方法(尽管他不需要用)。