11、单一职责原则

1、如何理解单一职责原则(SRP)

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

注意:这个原则描述的对象包含两个,一个是类(class),一个是模块(module),关于这两个概念,在专栏中,有两种理解方式
一种理解是:把模块看作比类更加抽象的概念,类也可以看作模块
另一种理解是:把模块看作比类更加粗粒度的代码块,模块中包含多个类,多个类组成一个模块

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

我举一个例子来解释一下:一个类里既包含订单的一些操作,又包含用户的一些操作
而订单和用户是两个独立的业务领域模型,我们将两个不相干的功能放到同一个类中,那就违反了单一职责原则
为了满足单一职责原则,我们需要将这个类拆分成两个粒度更细、功能更加单一的两个类:订单类和用户类

2、如何判断类的职责是否足够单一

从刚刚这个例子来看,单一职责原则看似不难应用,那是因为我举的这个例子比较极端,一眼就能看出订单和用户毫不相干
但大部分情况下,类里的方法是归为同一类功能,还是归为不相关的两类功能,并不是那么容易判定的
在真实的软件开发中,对于一个类是否职责单一的判定,是很难拿捏的,我举一个更加贴近实际的例子来给你解释一下

2.1、示例

在一个社交产品中,我们用下面的 UserInfo 类来记录用户的信息,你觉得,UserInfo 类的设计是否满足单一职责原则呢

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;   // 详细地址

    // ... 省略其他属性和方法 ...
}

对于这个问题,有两种不同的观点,哪种观点更对呢

  • UserInfo 类包含的都是跟用户相关的信息,所有的属性和方法都隶属于用户这样一个业务模型,满足单一职责原则
  • 地址信息在 UserInfo 类中所占的比重比较高
    可以继续拆分成独立的 UserAddress 类,UserInfo 只保留除 Address 之外的其他信息,拆分之后的两个类的职责更加单一

要从中做出选择,我们不能脱离具体的应用场景

  • 如果在这个社交产品中,用户的地址信息跟其他信息一样,只是单纯地用来展示,那 UserInfo 现在的设计就是合理的
  • 但如果这个社交产品发展得比较好,之后又在产品中添加了电商的模块,用户的地址信息还会用在电商物流中
    那我们最好将地址信息从 UserInfo 中拆分出来,独立成用户物流信息(或者叫地址信息、收货信息等)
  • 我们再进一步延伸一下,如果做这个社交产品的公司发展得越来越好,公司内部又开发出了跟多其他产品(可以理解为其他 App)
    公司希望支持统一账号系统,也就是用户一个账号可以在公司内部的所有产品中登录
    这个时候,我们就需要继续对 UserInfo 进行拆分,将跟身份认证相关的信息(比如 email、telephone 等)抽取成独立的类

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

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

2.2、总结

我们可以先写一个粗粒度的类,满足业务需求
随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候,我们就可以将这个粗粒度的类,拆分成几个更细粒度的类,这就是所谓的持续重构

听到这里你可能会说,这个原则如此含糊不清、模棱两可,到底该如何拿捏才好啊
我这里还有一些小技巧,能够很好地帮你,从侧面上判定一个类的职责是否够单一
而且我个人觉得,下面这几条判断原则,比起很主观地去思考类是否职责单一,要更有指导意义、更具有可执行性

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

3、类的职责是否设计得越单一越好

为了满足单一职责原则,是不是把类拆得越细就越好呢,答案是否定的

3.1、示例

我们还是通过一个例子来解释一下,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() {
        gson = new Gson();
    }

    public String serialize(Map<String, String> object) {
        StringBuilder sb = new StringBuilder();
        sb.append(IDENTIFIER_STRING);
        sb.append(gson.toJson(object));
        return sb.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);
    }
}

如果我们想让类的职责更加单一,我们对 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 sb = new StringBuilder();
        sb.append(IDENTIFIER_STRING);
        sb.append(gson.toJson(object));
        return sb.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);
    }
}

3.2、总结

虽然经过拆分之后,Serializer 类和 Deserializer 类的职责更加单一了,但也随之带来了新的问题

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

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

posted @ 2023-06-25 14:17  lidongdongdong~  阅读(23)  评论(0编辑  收藏  举报