• 设计原则之-单一职责原则


    全网最全最细的【设计模式】总目录,收藏起来慢慢啃,看完不懂砍我

    什么是单一职责原则

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

    一个类只负责完成一个职责或者功能。也就是说,不要设计大而全的类,要设计粒度小、功能单一的类。换个角度来讲就是,一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类。

    案例

    // IUserInfoDao
    User selectUser(String id);
    List<User> selectUsers();
    int insertOne(User user);
    int updateOne(User user);
    Org selectOrg(String id);
    int selectOrgCount(Org org);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    相信很多小伙伴一眼就看出以上用户接口有问题了,即用户和机构没有分开设计。也就是说,接口的职责并不是单一的,而是包含两个职责(功能),用户机构。

    如果按照单依职责原则,应该将用户和机构分开,关于用户的操作放在一个接口,关于机构的操作放在另一个接口。

    但是很多时候,一个类的职责划分的并没有很明确,比如说:

    // IUserInfoDao
    User selectUserAndOrg(String id);
    
    // User
    private String id;
    private String name;
    private String mobile;
    ...
    private String orgId;
    private String orgName;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    当一个关联查询涉及user信息、org信息时,此时将这样一个操作同时放在User类中合不合适?这样是否违背单一职责原则呢?

    有人会说,对于org的信息来说,可能对user只是展示的作用,放在user类中并无不可;也有人说,org信息占比太多,应该单独抽离出一个org类。

    我们可以再延伸一步,当公司做的原来越大,user信息已经拆分成两部分,原来的id、name等等基础信息为一部分,mobile、address等作为附加信息为另一部分,此时是不是应该也算是不符合单一职责原则了呢?

    我们可以总结出,不同的应用场景、不同阶段的需求背景下,对同一个类的职责是否单一的判定,可能都是不一样的。在某种应用场景或者当下的需求背景下,一个类的设计可能已经满足单一职责原则了,但如果换个应用场景或着在未来的某个需求背景下,可能就不满足了,需要继续拆分成粒度更细的类。

    除此之外,从不同的业务层面去看待同一个类的设计,对类是否职责单一,也会有不同的认识。比如,例子中的 User 类。如果我们从“用户”这个业务层面来看,User 包含的信息都属于用户,满足职责单一原则。如果我们从更加细分的“用户展示信息”“地址信息”“登录认证信息”等等这些更细粒度的业务层面来看,那 User 就应该继续拆分。

    如何判断一个类的职责是否单一?

    上面的案例相信我们也都明白,评价一个类的职责是否足够单一,我们并没有一个非常明确的、可以量化的标准,可以说,这是件非常主观、仁者见仁智者见智的事情。实际上,在真正的软件开发中,我们也没必要过于未雨绸缪,过度设计。所以,我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类。这就是所谓的持续重构。

    下面这几条判断原则,比起很主观地去思考类是否职责单一,要更有指导意义、更具有可执行性:

    • 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分;
    • 类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;
    • 私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性;
    • 比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰;
    • 类中大量的方法都是集中操作类中的某几个属性,比如,在 UserInfo 例子中,如果一半的方法都是在操作 address 信息,那就可以考虑将这几个属性和对应的方法拆分出来。

    类的职责是否越单一越好?

    Serialization 类实现了一个简单协议的序列化和反序列功能,具体代码如下:

    
    /**
     * Protocol format: identifier-string;{gson string}
     * For example: UEUEUE;{"a":"A","b":"B"}
     */
    public class Serialization {
      private static final String IDENTIFIER_STRING = "UEUEUE;";
      private Gson gson;
      
      public Serialization() {
        this.gson = new Gson();
      }
      
      public String serialize(Map<String, String> object) {
        StringBuilder textBuilder = new StringBuilder();
        textBuilder.append(IDENTIFIER_STRING);
        textBuilder.append(gson.toJson(object));
        return textBuilder.toString();
      }
      
      public Map<String, String> deserialize(String text) {
        if (!text.startsWith(IDENTIFIER_STRING)) {
            return Collections.emptyMap();
        }
        String gsonStr = text.substring(IDENTIFIER_STRING.length());
        return gson.fromJson(gsonStr, Map.class);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    如果我们想让类的职责更加单一,我们对 Serialization 类进一步拆分,拆分成一个只负责序列化工作的 Serializer 类和另一个只负责反序列化工作的 Deserializer 类。拆分后的具体代码如下所示:

    
    public class Serializer {
      private static final String IDENTIFIER_STRING = "UEUEUE;";
      private Gson gson;
      
      public Serializer() {
        this.gson = new Gson();
      }
      
      public String serialize(Map<String, String> object) {
        StringBuilder textBuilder = new StringBuilder();
        textBuilder.append(IDENTIFIER_STRING);
        textBuilder.append(gson.toJson(object));
        return textBuilder.toString();
      }
    }
    
    public class Deserializer {
      private static final String IDENTIFIER_STRING = "UEUEUE;";
      private Gson gson;
      
      public Deserializer() {
        this.gson = new Gson();
      }
      
      public Map<String, String> deserialize(String text) {
        if (!text.startsWith(IDENTIFIER_STRING)) {
            return Collections.emptyMap();
        }
        String gsonStr = text.substring(IDENTIFIER_STRING.length());
        return gson.fromJson(gsonStr, Map.class);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    虽然经过拆分之后,Serializer 类和 Deserializer 类的职责更加单一了,但也随之带来了新的问题。如果我们修改了协议的格式,数据标识从“UEUEUE”改为“DFDFDF”,或者序列化方式从 JSON 改为了 XML,那 Serializer 类和 Deserializer 类都需要做相应的修改,代码的内聚性显然没有原来 Serialization 高了。而且,如果我们仅仅对 Serializer 类做了协议修改,而忘记了修改 Deserializer 类的代码,那就会导致序列化、反序列化不匹配,程序运行出错,也就是说,拆分之后,代码的可维护性变差了。

    实际上,不管是应用设计原则还是设计模式,最终的目的还是提高代码的可读性、可扩展性、复用性、可维护性等。我们在考虑应用某一个设计原则是否合理的时候,也可以以此作为最终的考量标准。

    总结

    我们常说的“高内聚”,“低耦合”,其实相关的都放在一起叫高内聚,不放在一起叫低内聚;不相关的放在一起叫高耦合,不相关的不放在一起叫低耦合。

    很多设计原则并不是一定要朝着这个方向去设计,而是随着当前的业务、以及靠着自己的经验来判断。这并不是一个1+1=2定性的东西,而是非常灵活。

    所以说,掌握了设计原则,靠的是大量的实践。

    参考资料

    王争老师《设计模式之美》

  • 相关阅读:
    基于arm-linux-gcc版本,音乐视频播放器mplayer
    7.基于SpringBoot3+Security6+JWT实现鉴权机制(一)
    virtio-blk简易驱动
    Jmeter常用的两大性能测试场景你都知道吗?
    RocketMq最强总结 带你rocket从入门到入土为安
    CSS核心使用
    Halcon转OpenCV实例--保险丝颜色识别(附源码)
    SSL证书品牌五花八门,该选哪个好呢?
    什么是操作系统的微内核
    罗德与施瓦茨与中关村泛联院合作开展6G技术研究与早期验证
  • 原文地址:https://blog.csdn.net/A_art_xiang/article/details/127608470