美文网首页
2022-01-01设计原则--单一职责与接口隔离原则总结

2022-01-01设计原则--单一职责与接口隔离原则总结

作者: 竹blue | 来源:发表于2022-01-01 18:36 被阅读0次

    单一职责(SRP)

    • 如何理解单一职责原则(SRP)?

      单一职责原则的英文是 Single Responsibility Principle,缩写为 SRP。这个原则的英文描述是这样的:A class or module should have a single responsibility。如果我们把它翻译成中文,那就是:一个类或者模块只负责完成一个职责(或者功能)。

      注意,这个原则描述的对象包含两个,一个是类(class),一个是模块(module)。关于这两个概念,有两种理解方式。一种理解是:把模块看作比类更加抽象的概念,类也可以看作模块。另一种理解是:把模块看作比类更加粗粒度的代码块,模块中包含多个类,多个类组成一个模块,不管哪种理解道理是想通的,下面以类作为分析对象,模块自行引申即可。

      一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。

    • 如何判断类的职责是否足够单一?

    不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。所以我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类(持续重构)。

    实际上,一些侧面的判断指标更具有指导意义和可执行性,比如,出现下面这些情况就有可能说明这类的设计不满足单一职责原则:

    1. 类中的代码行数、函数或者属性过多;
    2. 类依赖的其他类过多,或者依赖类的其他类过多;
    3. 私有方法过多;
    4. 比较难给类起一个合适的名字;
    5. 类中大量的方法都是集中操作类中的某几个属性。
    /**
     * UserInfo类
     *
     * 该类是否满足单一职责?
     *
     * 分析问题要结合实际的应用场景:如果在这个社交产品中,用户的地址信息跟其他信息一样,只是单纯地用来展示,那 UserInfo 现在的设计就是合理的。
     * 但是,如果这个社交产品发展得比较好,之后又在产品中添加了电商的模块,用户的地址信息还会用在电商物流中,那我们最好将地址信息从 UserInfo 中拆分出来,独立成用户物流信息(或者叫地址信息、收货信息等)。
     *
     */
    @Getter
    @Setter
    public class UserInfo {
    
        private long userId;
        private String username;
        private String email;
        private String telephone;
        private long createTime;
        private long lastLoginTime;
        private String avatarUrl;
        private String provinceOfAddress; // 省
        private String cityOfAddress; // 市
        private String regionOfAddress; // 区
        private String detailedAddress; // 详细地址
    
    }
    
    • 类的职责是否设计得越单一越好?

      单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。

    /**
     * Serialization类
     *
     * 拆分过度问题:以序列化为例经过拆分之后,Serializer 类和 Deserializer 类的职责更加单一了,
     * 但也随之带来了新的问题。如果我们修改了协议的格式,数据标识从“UEUEUE”改为“DFDFDF”,或者序列
     * 化方式从 JSON 改为了 XML,那 Serializer 类和 Deserializer 类都需要做相应的修改,代码的
     * 内聚性显然没有原来 Serialization 高了。而且,如果我们仅仅对 Serializer 类做了协议修改,而
     * 忘记了修改 Deserializer 类的代码,那就会导致序列化、反序列化不匹配,程序运行出错,也就是说,
     * 拆分之后,代码的可维护性变差了。
     *
     *
     */
    public class Serialization {
        private static final String IDENTIFIER_STRING = "UEUEUE;";
    
        public String serialze(Map<String, String> object) {
            StringBuilder textBuilder = new StringBuilder(IDENTIFIER_STRING);
            textBuilder.append(JSON.toJSONString(object));
            return textBuilder.toString();
        }
    
        public Map<String, String> deserialize(String text){
            if(!text.startsWith(IDENTIFIER_STRING)){
                return Collections.emptyMap();
            }
    
            text = text.substring(IDENTIFIER_STRING.length());
    
            return JSON.parseObject(text,new TypeReference<HashMap<String,String>>(){});
        }
    }
    
    public class Serializer {
    
        private static final String IDENTIFIER_STRING = "UEUEUE;";
    
        public String serialze(Map<String, String> object) {
            StringBuilder textBuilder = new StringBuilder(IDENTIFIER_STRING);
            textBuilder.append(JSON.toJSONString(object));
            return textBuilder.toString();
        }
    
    }
    
    public class Deserializer {
        private static final String IDENTIFIER_STRING = "UEUEUE;";
    
    
        public Map<String, String> deserialize(String text){
            if(!text.startsWith(IDENTIFIER_STRING)){
                return Collections.emptyMap();
            }
    
            text = text.substring(IDENTIFIER_STRING.length());
    
            return JSON.parseObject(text,new TypeReference<HashMap<String,String>>(){});
        }
    }
    

    接口隔离原则(ISP)

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

      接口隔离原则的英文翻译是“ Interface Segregation Principle”,缩写为 ISP。Robert Martin 在 SOLID 原则中是这样定义它的:“Clients should not be forced to depend upon interfaces that they do not use。”直译成中文的话就是:客户端不应该被强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者。

      理解“接口隔离原则”的重点是理解其中的“接口”二字。这里有三种不同的理解。

      • 如果把“接口”理解为一组接口集合,可以是某个微服务的接口,也可以是某个类库的接口等。如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。
      /**
       * UserService接口
       *
       *  场景:用户系统提供了一组跟用户相关的 API 给其他系统使用,比如:注册、登录、获取用户信息等。现在,
       *  我们的后台管理系统要实现删除用户的功能,希望用户系统提供一个删除用户的接口。这个时候我们该如何来做呢?
       *
       *  分析:方案一:在 UserService 中新添加一个 deleteUserByCellphone() 或 deleteUserById() 接口就可以了。
       *  这个方法可以解决问题,但是也隐藏了一些安全隐患,删除用户是一个非常慎重的操作,我们只希望通过后台管理系统来执行,
       *  所以这个接口只限于给后台管理系统使用,如果在没有鉴权的情况下,加限制地被其他业务系统调用,就有可能导致误删用户。
       *
       *  方案二:在没有鉴权情况下可以从代码层面规避上述风险,具体可以参照接口隔离原则,调用者不应该强迫依赖它不需要的接口,
       *  将删除接口单独放到另外一个接口 RestrictedUserService 中,然后将 RestrictedUserService 只打包提供给后台
       *  管理系统来使用。
       *
       */
      
      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 BackgroundUserServiceImpl implements UserService, RestrictedUserService {
      
          @Override
          public boolean deleteUserByCellphone(String cellphone) {
              return false;
          }
      
          @Override
          public boolean deleteUserById(long id) {
              return false;
          }
      
          @Override
          public boolean register(String cellphone, String password) {
              return false;
          }
      
          @Override
          public boolean login(String cellphone, String password) {
              return false;
          }
      
          @Override
          public UserInfo getUserInfoById(long id) {
              return null;
          }
      
          @Override
          public UserInfo getUserInfoByCellphone(String cellphone) {
              return null;
          }
      }
      
      • 如果把“接口”理解为单个 API 接口或函数,函数的设计要功能单一,不要将多个不同的功能逻辑在一个函数中实现。部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。
      /**
       * Statistics类
       *
       * 接口设计分析:count() 函数的功能不够单一,包含很多不同的统计功能,比如,求最大值、最小值、平均值等
       * 场景一:如果在项目中,对每个统计需求,Statistics 定义的那几个统计信息都有涉及,那 count()
       * 函数的设计就是合理的。
       *
       * 场景二:如果每个统计需求只涉及 Statistics 罗列的统计信息中一部分,比如,有的只需要用到 max、
       * min、average 这三类统计信息,在这个应用场景下,count() 函数的设计就有点不合理了,这种场景下
       * 需要将其拆分成粒度更细的多个统计函数。
       *
       * 总结:ISP提供了一种判断接口是否职责单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只
       * 使用部分接口或接口的部分功能,那接口的设计就不够职责单一。
       *
       */
      @Getter
      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) {
              Statistics statistics = new Statistics();
              //求最大值
              statistics.setMax(2L);
              // 最小值
              statistics.setMin(0L);
              // 平均值
              statistics.setAverage(1L);
              return statistics;
          }
      
          /**
           * 场景二下合理
           *
           * @param dataSet
           * @return
           */
          public Long max(Collection<Long> dataSet) {
              return 2L;
          }
      
          public Long min(Collection<Long> dataSet) {
              return 0L;
          }
      
          public Long average(Collection<Long> dataSet) {
              return 1L;
          }
      
          public void setMax(Long max) {
              this.max = max;
          }
      
          public void setMin(Long min) {
              this.min = min;
          }
      
          public void setAverage(Long average) {
              this.average = average;
          }
      }
      
      • 如果把“接口”理解为 面向对象编程(OOP) 中的接口,也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。
      /**
       * Application类
       *
       * 背景:假设我们的项目中用到了三个外部系统:Redis、MySQL、Kafka。每个系统都对应一系列配置信息,
       *      比如地址、端口、访问超时时间等。为了在内存中存储这些配置信息,供项目中的其他模块来使用,我
       *      们分别设计实现了三个 Configuration 类:RedisConfig、MysqlConfig、KafkaConfig
       *
       * 需求:
       *      1.希望支持 Redis 和 Kafka 配置信息的热更新。所谓“热更新(hot update)”就是,如果在配
       *      置中心中更改了配置信息,我们希望在不用重启系统的情况下,能将最新的配置信息加载到内存中(也就
       *      是 RedisConfig、KafkaConfig 类中)。
       *
       *      2.监控功能需求。通过命令行来查看 Zookeeper 中的配置信息是比较麻烦的。所以,我们希望能有一
       *      种更加方便的配置信息查看方式。我们可以在项目中开发一个内嵌的 SimpleHttpServer,输出项目的
       *      配置信息到一个固定的 HTTP 地址,比如:http://127.0.0.1:2389/config 。我们只需要在浏览
       *      器中输入这个地址,就可以显示出系统的配置信息。不过,出于某些原因,我们只想暴露 MySQL 和 Redis
       *      的配置信息。
       *
       */
      public class Application {
      
          private static ConfigSource configSource = new ZookeerConfigSource();
      
          private static final RedisConfig redisConfig = new RedisConfig(configSource);
          private static final KafkaConfig kafkaConfig = new KafkaConfig(configSource);
          private 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();
      
              SimpleHttpServer simpleHttpServer = new SimpleHttpServer("127.0.0.1",2389);
              simpleHttpServer.addViewer("/config",redisConfig);
              simpleHttpServer.addViewer("/config",mySqlConfig);
          }
       
      }
      
      /**
       * Updater热更新接口
       */
      public interface Updater {
          /**
           * 热部署,从configSource加载配置到address/timeout/maxTotal
           */
          void update();
      }
      /**
       * Viewer监控接口
       */
      public interface Viewer {
          /**
           * 监控-输出文本信息
           */
          String outputInPlainText();
      
          /**
           * 监控-输出监控项
           */
          Map<String,String> output();
      }
      
      //接口处理类
      public class ScheduledUpdater {
          private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
      
          private long initialDelayInSeconds;
      
          private long periodInSeconds;
      
          private Updater updater;
      
          public ScheduledUpdater(Updater updater, long initialDelayInSeconds, long periodInSeconds) {
              this.initialDelayInSeconds = initialDelayInSeconds;
      
              this.periodInSeconds = periodInSeconds;
      
              this.updater = updater;
          }
      
          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>> viewerMap = new HashMap<>();
      
          public SimpleHttpServer(String host, int port) {
              this.host = host;
              this.port = port;
          }
      
          public void addViewer(String urlDirectory, Viewer viewer) {
              if (!viewerMap.containsKey(urlDirectory)) {
                  viewerMap.put(urlDirectory, new ArrayList<Viewer>());
              }
      
              viewerMap.get(urlDirectory).add(viewer);
      
          }
      
          public void run(){
              // 输出项目的配置信息到一个固定的 HTTP 地址
              // 比如:http://127.0.0.1:2389/config 。
              // 我们只需要在浏览器中输入这个地址,就可以显示出系统的配置信息。
          }
      }
      
      //config配置类
      @Getter
      public abstract class AbstractConfig {
          /**
           * 配置中心(比如zookeeper)
           */
          protected ConfigSource configSource;
      
          protected String address;
      
          protected int timeout;
      
          protected int maxTotal;
      
      }
      
      public class RedisConfig extends AbstractConfig implements Updater, Viewer {
      
          public RedisConfig(ConfigSource configSource) {
              super();
              super.configSource = configSource;
          }
      
          /**
           * 热部署,从configSource加载配置到address/timeout/maxTotal
           */
          @Override
          public void update() {
              super.address = configSource.getAddress();
              super.timeout = configSource.getTimeout();
              super.maxTotal = configSource.getMaxTotal();
          }
      
          /**
           * 监控-输出文本信息
           */
          @Override
          public String outputInPlainText() {
              return JSON.toJSONString(this);
          }
      
          /**
           * 监控-输出监控项
           */
          @Override
          public Map<String, String> output() {
              return JSON.parseObject(this.outputInPlainText(),
                  new TypeReference<HashMap<String, String>>(){});
          }
      }
      
      public class MysqlConfig extends AbstractConfig implements Viewer {
      
          public MysqlConfig(ConfigSource configSource) {
              super();
              super.configSource = configSource;
          }
        
          @Override
          public String outputInPlainText() {
              return JSON.toJSONString(this);
          }
      
          @Override
          public Map<String, String> output() {
              return JSON.parseObject(this.outputInPlainText(),
                  new TypeReference<HashMap<String, String>>(){});
          }
      }
      
      public class KafkaConfig extends AbstractConfig implements Updater {
      
          public KafkaConfig(ConfigSource configSource) {
              super();
              super.configSource = configSource;
          }
      
          @Override
          public void update() {
              super.address = configSource.getAddress();
              super.timeout = configSource.getTimeout();
              super.maxTotal = configSource.getMaxTotal();
          }
      }
      
    2. 接口隔离原则与单一职责原则的区别

      单一职责原则针对的是模块、类、接口的设计。接口隔离原则相对于单一职责原则,一方面更侧重于接口的设计,另一方面它的思考角度也是不同的。接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一

    相关文章

      网友评论

          本文标题:2022-01-01设计原则--单一职责与接口隔离原则总结

          本文链接:https://www.haomeiwen.com/subject/paxaqrtx.html