面向对象设计之单一职责原则

设计模式专栏:http://t.csdnimg.cn/6sBRl

目录

1.单一职责原则的定义和解读

2.如何判断类的职责是否单一

3.类的职责是否越细化越好

4.总结


1.单一职责原则的定义和解读

        单一职责原则(Single Responsibility Principle,SRP)的描述:一个类或模块只负责完一个职责(或功能)(A class or module should have a single reponsibility)。
        注意,单一职责原则描述的对象有两个:类(class)和模块(module)。关于这两个概念我们有两种理解方式。一种理解方式是把模块看作比类更加抽象的概念,把类看作一种模块;另一种理解方式是把模块看作比类更粗粒度的代码块,多个类组成一个块。
        无论哪种理解方式,单一职责原则在应用这两个描述对象时,原理是相通的。为了方讲解,我们只从“类”设计的角度讲解如何应用单一职责原则。对于“模块”,读者可以自行理解。
        单一职责原则是指一个类负责完成一个职责或功能。也就是说,我们不要设计大而全的类,要设计粒度小、功能单一的类。换个角度来讲,如果一个类包含两个或两个以上业务不相干的功能,那么我们就可以认为它的职责不够单一,应该将其拆分成多个粒度更小的功能单一的类。
        例如,某类既包含对订单的一些操作,又包含对用户的一些操作。而订单和用户是两个独立的业务领域模型,将两个不相干的功能放到同一个类中,就违反了单一职责原则。为了满足单一职责原则,我们需要将这个类拆分成粒度更小的功能单一的两个类:订单类和用户类。

2.如何判断类的职责是否单一

        在上章节举的例子简单,我们立即就能看出订单和用户毫不相干。但大部分情况下,类中的方法是归为同一类功能,还是归为不相关的两类功能,并不是那么容易判定。在真实的软件开发中,一个类是否职责单一的判定是很难的。我们用一个贴近真实开发的例子来解释类的职责是否单一的判定问题。
        在某个社交产品中,我们用UserInfo类记录用户信息、那么,读者觉得以下 UserInfo类的设计是否满足单一职责原则呢?

public class UserInfo {
    private long userId;
    private String username,
    private String email;
    private String telephone;
    private long createTime;
    private long lastloginrime;
    private String avatarUrl;
    private String provinceofAddress; //省
    private String cityofaddress; //市
    private String regionofAddress; //
    private String detailedAddress;//详细地址
    //...省略其他属性和方法...
}

        对于这个问题,我们有两种不同的观点。一种观点是UserInfo类包含的是与用户相关的信息,所有的属性和方法都隶属于用户这样一个业务模型、满足单一职责原则;另一种观点是地址信息在UserInfo类中所占的比例较高,可以继续拆分成独立的UserAddress类,而UserInfo只保留除地址信息之外的其他信息,拆分后的两个类的职责变得单一。
        对于上述两种观点,哪种观点是合理的呢?实际上,如果我们想要从中做出选择,就不能脱离具体的应用场景。如果在这个社交产品中,用户的地址信息与用户其他信息一样,只是用来进行信息展示,它们同时被使用,那么UserInfo类目前的设计就是合理的。但是,假如这个社交产品发展得比较好,之后又在该产品中添加了电商功能模块,用户的地址信息不仅用于展示,还会独立地应用在电商的物流中,此时最好将地址信息从Userlnfo类中拆分出来,独立成为物流信息(或者称为地址信息、收货信息等)。
        再进一步,假如这个社交产品所属的公司发展壮大,该公司又开发了很多其他产品(可以理解为其他App)。该公司希望其所有产品支持统一账号系统,即用户使用同一个账号可以在该公司的所有产品登录。此时,就需要继续对UserInfo类进行拆分,将与身份认证相关的信息(如email、telephone等)抽取成独立的类。
        从上面的例子中,我们总结得出:在不同的应用场景和不同阶段的需求背景下,对同一个类的职责是否单一的判定可能是不一样的。在某种应用场景或当下的需求背景下,一个类的设计可能已经满足单一职责原则了,但如果换个应用场景或在未来的某个需求背景下,就可能不满足单一职责原则了,需要继续拆分成粒度更小的类。
        除此之外,在从不同的业务层面看同一个类的设计时,我们对类是否职责单一的判定会有不同的认识。对于上面例子中的 UserInfo类,如果从“用户”业务层面来看,UserInfo 类包含的信息都属于用户,那么满足职责单一原则;如果从“用户展示信息”、“地址信息”、“登录认证信息”等更细粒度的业务层面来看,那么UserInfo 类就不满足单一职责原则,应该继续拆分。

        综上所述,评价一个类的职责是否单一,并没有一个明确的、可量化的标准。实际上,在真正的软件开发中,我们没必要过度设计(粒度过细)。我们可以先编写一个粗粒度的类,满足当下的业务需求即可。随着业务的发展,如果这个粗粒度的类越来越复杂,代码越来越多,那么我们在这时再将这个粗粒度的类拆分成几个细粒度的类即可。

        对于职责是否单一的判定,存在一些判定规则,如下所示:
        1) 如果类中的代码行数、函数或属性过多,影响代码的可读性和可维护性,就需要考虑对类进行拆分。
        2) 如果某个类依赖的其他类过多,或者依赖某个类的其他类过多,不符合高内聚、低合的代码设计思想,就需要考虑对该类进行拆分。
        3) 如果类中的私有方法过多,就需要考虑将私有方法独立到新的类中,并设置为public方法,供更多的类使用,从而提高代码的复用性。
        4) 如果类很难准确命名(很难用的个业务名词概括),或者只能用 Manager、Context之类的笼统的词语来命名,就说明类的职责定义不够清晰。
        5) 如果类中的大量方法集中操作其中几个属性(如上面的UserInfo类的例子中,加入很多方法只操作 address 信息),就可以考虑将这些属性和对应的方法拆分出来。

3.类的职责是否越细化越好

        为了满足单一职责原则,是不是把类拆分得越细就越好呢?答案是否定的、我们举例解释,示例代码如下所示。Serialization 类实现了一个简单协议的序列化和反序列功能。
 

/**
  *Protocol format:identifer-string;(gson string)
  *For example: UUEUE;{"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(){
        thís.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 类都需要做相应的修改,代码的内聚性显然没有之前高了。而且,如果我们对 Serializer 类做了协议修改,而忘记修改Deserializer 类的代码,就会导致序列化和反序列化不匹配,程序运行出错,也就是说,拆分之后,代码的可维护性变差了。

        实际上,无论是应用设计原则还是设计模式,最终的目的都是为了提高代码的可读性、可扩展性、复用性和可维护性等。在判断应用某一个设计原则是否合理时,我们可以以此作为最终的评价标准。

4.总结

        单一职责原则的核心思想是:一个类或者一个模块应该只负责一个功能领域中的相应职责,或者说,一个类应该只有一个引起它变化的原因。

        在软件系统中,如果一个类(无论是模块、方法还是其他更小的单元)承担的职责过多,那么这个类的复用性就会降低,因为不同的职责可能对应着不同的使用场景。此外,当这个类的某个职责发生变化时,可能会影响到其他职责的运作,从而增加出错的可能性。因此,根据单一职责原则,我们应该将不同的职责分离,并将它们封装在不同的类中。

        例如,在微服务架构中,每个微服务通常只负责一个或少数几个紧密相关的功能,这就是单一职责原则的具体实践。通过将复杂的业务拆分成多个功能单一的微服务,我们可以提高系统的可维护性、可扩展性和可复用性。

        总的来说,单一职责原则有助于我们设计出更加灵活、易于维护和扩展的软件系统。在实际编程中,我们应该时刻注意遵守这一原则,避免将过多的职责集中在一个类中。

相关推荐

  1. 面向对象设计单一职责原则

    2024-03-30 16:04:02       24 阅读
  2. 设计模式-单一职责原则

    2024-03-30 16:04:02       17 阅读
  3. C++设计模式-单一职责原则

    2024-03-30 16:04:02       29 阅读
  4. 面向对象设计里氏替换原则

    2024-03-30 16:04:02       19 阅读
  5. 面向对象设计开闭原则

    2024-03-30 16:04:02       20 阅读
  6. 设计模式学习笔记 - 设计原则 - 1.单一职责原则

    2024-03-30 16:04:02       24 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-03-30 16:04:02       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-03-30 16:04:02       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-03-30 16:04:02       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-03-30 16:04:02       20 阅读

热门阅读

  1. kubuntu23.10安装sdl2及附加库和 sfml2.5.1

    2024-03-30 16:04:02       18 阅读
  2. 动态堆栈类及括号匹配(考察类的构建与应用)

    2024-03-30 16:04:02       18 阅读
  3. python知识点记录

    2024-03-30 16:04:02       21 阅读
  4. JVM基础

    JVM基础

    2024-03-30 16:04:02      18 阅读
  5. contextlib.redirect_stdout 使用

    2024-03-30 16:04:02       16 阅读
  6. docker-compose运行mysql

    2024-03-30 16:04:02       15 阅读
  7. 算法——图论:判断二分图(染色问题)

    2024-03-30 16:04:02       19 阅读
  8. 什么是站群服务器?

    2024-03-30 16:04:02       16 阅读
  9. vue3父子组件之间的传值方式

    2024-03-30 16:04:02       22 阅读
  10. C# 到异常处理 暂时告一段落 开始窗体的学习

    2024-03-30 16:04:02       20 阅读
  11. 每日一题:C语言经典例题之鸡兔同笼

    2024-03-30 16:04:02       20 阅读
  12. Grok - X AI 314B大模型

    2024-03-30 16:04:02       26 阅读
  13. 【SQL】COUNT()函数 用法详解

    2024-03-30 16:04:02       23 阅读