暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

单一职责原则:为什么作者要3次修改定义?

猿java 2022-10-06
132

Weiki

读完需要

15
分钟

速读仅需 8 分钟

Hello,大家伙,我是 猿java,一个践行终身学习的程序员。

   
在日常开发中,经常会听到有经验的技术人员念叨:设计一定要注意单一职责,那么单一是不是一个类只需要干一件事情呢?今天我们来聊聊单一职责到底怎么单一,为什么作者要对该原则的定义进行 3次修改。


1. 单一职责原则的定义


   

单一职责原则,源于 Robert C. Martin 的经典之作《Clean Architecture》中的 SOLID 原则,中文翻译版为《架构整洁之道》,SOLID 实际上是五个设计原则首字母的缩写,它们分别是:

  • 单一职责原则(Single responsibility principle, SRP)

  • 开放封闭原则(Open–closed principle, OCP)

  • Liskov 替换原则(Liskov substitution principle, LSP)

  • 接口隔离原则(Interface segregation principle, ISP)

  • 依赖倒置原则(Dependency inversion principle, DIP)

单一,很容易让人望文生义,理解成只干一件事情。既然 SOLID 原则是由 Robert C. Martin 提出和完善的,那么我们先看看作者对单一职责原则是如何描述的。作者第一次对单一职责描述的原文:

    The Single Responsibility Principle (SRP)
     states that each software module should 
     have one and only one reason to change.

    原文意思:单一职责原则 (SRP) 指出,任何一个软件模块都应该有且只有一个变化的理由。

    可是,软件设计是一门关注长期变化的学问,变化是我们最不想面对却又不得不面对的事情,比如在现实环境中,软件系统为了满足用户和所有者的要求,势必会作出各种修改,而系统的用户或所有者就是该设计原则中所指的"被修改的原因",因此“且只有一个修改的理由”的约束似乎很难满足现实的需求。

    于是,单一职责又被作者重新描述为:

      The single responsibility principle states that 
      every module or class should have responsibility
      over a single part of the functionality provided
      by the software, and that responsibility should
      be entirely encapsulated by the class

      原文意思:单一职责原则指出每个模块或类都应该对软件提供的单个功能部分负责,并且该职责应该完全由类封装。

      作者此次的定义中提及了“单个功能应该完全由类封装”,那么,我们是用一个类还是多个类去封装呢?这个定义看上去似乎还是会产生误解。

      于是,作者对单一职责进行了第三次描述:

        Each module should only be responsible to one actor.

        原文意思:任何一个软件模块都应该只对某一类行为者负责。

        作者在第 3次对单一职责的描述中使用了“软件模块”这个概念,那么“软件模块”是什么呢?

        在大部分情况下,“软件模块”可以简单理解为一个源代码文件,比如 java 中的类,接口,方法等;对于不使用源码文件存储程序的语言,可以理解成一组紧密相关的函数和数据结构。

        所以,作者第 3次对单一职责的描述,可以更直白地表达成:某个类或者某个函数(方法),再或者某种数据结构只能干一类事情。


        2. 违反单一职责


           

        为了更好地理解 “任何一个软件模块都应该只对某一类行为者负责” 这个描述,我们先来看一个违反单一职责的 Java 反例代码:

        假如某程序猿设计了一个 Employee 员工类并且类中包含 3个方法,代码如下:

          /**
          * 员工类
          */
          public class Employee {
            // 计算员工薪酬
          public Money calculatePay();

            // 将数据存储到企业数据库中
          public void save();

          // 用于促销活动发布
            public void postEvent();
          }

          乍一看,这个类设计得还挺符合实际业务,员工有计算薪酬、保存数据、发布促销等行为。但是,仔细推敲一下就会发现类中的 3个方法对应了三类不同的行为,Employee 类将三类行为耦合在一起,违反了单一职责原则。

          计算员工薪酬本来是财务的工作,假如一个技术人员不小心调用了 calculatePay()方法,把每个员工的薪酬计算成了实际工资的 2倍,这将会是一个灾难性的问题。另外,假如新的需求来了,要求员工能够导出报表,于是需要在 Employee类中增加一个新的方法,Employee类会发展成如下代码:

            /**
            * 员工类
            */
            public class Employee {
              // 计算员工薪酬
            public Money calculatePay();

            // 将数据存储到企业数据库中
            public void save();

               // 用于促销活动发布
            public void postEvent();

              // 导出报表
              void exportReport();
            }

            如果,需求一个接一个的过来,Employee 类就得一次又一次的变动,这样会带来怎样的后果呢?

            一方面,Employee 类会不断的膨胀;

            另一方面,内部的实现会越来越复杂,可能需求完全不同,却要在同一个类上不停的改动;

            可以联想一下你的工作是否也有过类似的设计,把很多不同的行为都耦合到一个类中,然后随着业务的发展,该类急剧膨胀,最后到了无法维护的地步。


            3. 解决方案


               

            对于上面违反单一职责的问题,我们有很多不同的方法来解决,按照单一职责的要求:一个软件模块只能对一类行为负责。因此,需要把 Employee类中方法按类别分开,因为相同原因而发生变化的行为聚集在一起,因不同原因而改变的行为分开,这是一个内聚和耦合的过程。最终,Employee类被拆解成下面 3个类:

              /**
               * 财务人员
               */
              public class FinanceStaff {
                // 计算员工薪酬
              public Money calculatePay();
              }


              /**
               * 技术人员
               */
              public class TechnicalStaff {
                   // 将数据存储到企业数据库中
              public void save();
              }


              /**
               * 操作员
               */
              public class OperatorStaff {
              // 用于促销活动发布
              public String postEvent();
              }


              从拆解后的 3个类,我们可以看出,每个类只需要关注和自己相关的行为。假如后期,需要增加一个薪资的发放功能,那么我们可以很轻易的知道,这个功能属于财务人员的行为,因此 FinanceStaff类会发展成下面的代码:

                /**
                * 财务人员
                 */
                public class FinanceStaff {
                  // 计算员工薪酬
                public Money calculatePay();

                  // 薪资发放
                  public void payrol();
                }

                通过上述违反单一职责的反例以及在解决方案中分析了单一职责的演进过程,我们可以清晰地看出软件模块是如何对一类行为负责。


                4. 总结


                   

                • 单一职责原则本质上就是要理解和分离关注点(行为)。

                • 单一职责原则可以应用于不同的层次,小到一个函数,大到一个系统,都可以用它来衡量我们的设计是否合理。

                映射到实际的工作中,我们可以把一个系统模块当做一个“软件模块”,因此它们的单一职责可能是系统级别,比如:订单系统只关注订单相关的行为,交易系统只关注交易相关的行为;同样,我们可以把类当做一个“软件模块”,它们单一职责就是类级别,比如:订单类,只关注订单相关的行为,用户类,只需要关注用户对应的行为。

                对于单一职责原则,我们需要根据具体的业务场景来灵活地进行抽象,工作中多关注一下这些原则,慢慢就能体会它的精髓。

                最后,呼应标题:为什么 Robert C. Martin 对单一职责的描述会经历 3次变更?

                这是因为软件设计也不是一成不变,一个模块最理想的状态是不改变,其次是少改变,所以,我们总是不断的重构让模块更少的改变。


                5. 鸣谢


                   

                如果你觉得本文章对你有帮助,感谢点赞,在读,或者转发给更多的好友,我们将为你呈现更多的硬核干货, 欢迎关注公众号:猿 java

                猿java精彩文章推荐:


                面试中回答ThreadLocal,看这一文就够了


                深度剖析IO多路复用机制


                如何设计一个秒杀系统?


                文章转载自猿java,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                评论