关于单一职责原则(SRP)

114 阅读4分钟

定义

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

这个原则有两个对象,一个是类,一个是对象。有两种理解⽅式。⼀种理解是:把模块看作⽐类更加抽象的概念,类也可以看作模块。另⼀种理解是:把模块看作⽐类更加粗粒度的代码块,模块中包含多个类,多个类组成⼀个模块

如何判断类是否足够单一:

比如下面的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中拆分出来,独立成为地址信息等。

综上所述,评价⼀个类的职责是否⾜够单⼀,我们并没有⼀个⾮常明确的、可以量化的标准,

可以说,这是件⾮常主观、仁者⻅仁智者⻅智的事情。实际上,在真正的软件开发中,我们也

没必要过于未⾬绸缪,过度设计。所以,我们可以先写⼀个粗粒度的类,满⾜业务需求。随着

业务的发展,如果粗粒度的类越来越庞⼤,代码越来越多,这个时候,我们就可以将这个粗粒

度的类,拆分成⼏个更细粒度的类。这就是所谓的持续重构

如何拿捏好单一职责?

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

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

比如下面这段:

/**
 * 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);
    }
}

如果我们想让类的职责更加单⼀,我们对 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);
    }
}

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

总结

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