10k

设计模式之美-课程笔记7-设计原则1-单一职责(SRP)

理论一:对于单一职责原则,如何判定某个类的职责是否够“单一”?

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

  1. 单一职责原则:Single responsibility Principle, SRP. A class or module should have a single responsibility. 一个类或模块只负责完成一个职责(或者功能)。关于模块和类有两种理解:
    1. 模块可以看做更抽象的类,类也可看成一个模块。
    2. 模块可以看做更粗粒度的代码块,包含多个类。
  2. 解读就是不要设计大而全的类,要设计粒度小,功能单一的类。比如一个类既包含订单的操作,又包含用户的操作,他们(用户和订单)是不同的领域模型,我们应该将其拆分。

如何判断类的职责足够单一

举个例子,比如我们要设计一个社交产品,其中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; // 详细地址
  // ...省略其他属性和方法...
}

反正我看着属性有点多,下面的省市区详细地址可以拆成另一个类。

作者提供的两个观点:1是和我的一样,地址信息占比较高,可以继续拆分成独立的UserAddress类,UserInfo只保留地址以外的信息。2. 是所有的信息都是用户相关的,所以满足SRP。

  1. 结合实际的应用业务场景。如何地址信息只是单纯的用来展示的,和其他的属性一样,那么就可以保留在UserInfo中。但是如果业务在拓展,有了电商模块,用户的地址信息还会用在电商物流中,那么我们最好将其分离出来,独立成物流信息。
  2. 如果业务继续发展,这个公司开发出来其他的一些应用,公司希望支持统一账号系统(一个账号可以登陆旗下所有app),那么我们还需要对UserInfo继续拆分,将身份认证的信息例如email, phone抽取成独立的的类。
  3. 不同的应用场景、不同阶段的需求背景下,对一个类的单一职责判定是不同的。
  4. 在实际开发中,我们可以先写一个粗粒度的类,满足业务需求。随着业务发展,如果粗粒度的类越来越庞大,代码越来越多,这个时候我们那就可以将这个类拆分成几个更细粒度的类。这就是所谓的持续重构。
  5. 为什么做拆分
    1. 类中的代码行数、函数或属性过多。影响代码的可读性可维护性。
    2. 类依赖的其他类过多,或者依赖这个类的其他类过多,不符合高内聚、低耦合的设计思想。
    3. 所有方法过多的,我们可以考虑是否可以将私有方法独立到新类中设置为公有方法,以供更多类使用提高代码复用。
    4. 比较难起名字。这时候说明类的职责不是十分清晰。
    5. 大量的方法都集中操作某几个属性,比如在UserInfo中一半的方法在操作address信息,那么就可以考虑将address相关的信息和方法拆分出来。
  6. 没有很具体的方法论和很定量说多少属性算多。如果要找一个标准,可以通过一个类是否已经让你感觉十分复杂和过多功能了来判断。

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

看个例子:

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

如果我们想拆分这个类,让职责更单一,拆分出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);
  }
}

拆分后功能更单一了,但是我们如果修改了协议的格式,数据标识从“UEUEUE”改为“DFDFDF”,或者序列化方式从 JSON 改为了 XML,那 Serializer 类和 Deserializer 类都需要做相应的修改,代码的内聚性显然没有原来 Serialization 高了

或者我们只改了desirializer。忘记了serializer,那么就会导致序列化反序列化不匹配。这其实是可维护性变差了。

总体还是要从代码的可读性,可维护性和可扩展性去考虑。

Thoughts? Leave a comment