单一职责原则(Single Pesponsibility Principle, SRP)是SOLID五大设计原则最容易被误解的一个。也许是名字的原因,很多程序员根据SRP这个名字想当然地认为这个原则是指:每个模块应该只做一件事。但这只是一个面向底层实现细节的设计原则,并不是SRP的全部。对于SRP的描述是:
任何一个软件模块都应该只对某一类行为者负责。
大部分情况下,对于 “软件模块” 的简单定义就是一个源代码文件(如.java文件和.py文件等),也可以指一组紧密相关的函数和数据结构。对于 “行为者” 可以是该模块的用户和利益相关者,只要是这些人对系统进行的变更是相似的,都可以归为同一类行为者。如何去理解SRP这一原则,下面通过两个反面案例来详细说明。
反面案例1:重复的假象
有一个员工管理模块Employee类,Employee类有三个函数:calculatePay(),reportHours()和save()。如图:
这三个函数分别对应的是三类不同的行为者,违反了SRP设计原则。
-
calculatePay()是计算工资函数:由财务部门制定,负责向CFO(首席财务官)汇报。
-
reportHours()是日常运营报告函数:由人力资源部门制定,负责向COO(首席营运官)汇报。
-
save()是持久化数据函数:由DBA制定,负责向CTO(首席技术官)汇报。
这三个函数被放在同一个源文件中,即同一个Employee类中,这样做实际上就等于使三类行为者的行为耦合在了一起,这有可能会导致CFO团队的需求变更影响到COO团队所依赖的功能。例如,calculatePay()函数和reportHours()函数使用了同样逻辑来计算员工的工时情况。开发者为了避免重复代码,通常会将该算法单独实现为一个名为regularHours()的函数。
那么,假设CFO团队需要修改工时的计算方法,而COO团队不需要这个修改,因为他们对工时的计算方法可能存在差异。这时,负责修改代码的开发者会注意到calculatePay()函数调用了regularHours()函数,当可能不会注意到该函数同时被reportHours()函数调用。
于是,该开发者就这样按照需求修改了工时的计算方法,同时CFO团队也验证了新算法可以正常工作。这项修改最终被成功部署上线了。但是,COO团队显然不知道这些变更事情的发生,在他们继续使用regularHours()产生报表时,随后就会发现数据错误的结果。
这类问题发生的根源就是因为我们将不同行为者所依赖的代码强凑合到了一起,对此,SRP强调这类代码一定要分开。
反面案例2:代码合并
一个拥有很多函数的源代码文件必然会经历很多次代码合并,如果这些函数分别服务于不同的行为者,会给代码合并带来很多问题(如代码冲突、功能影响),虽然有svn和git代码托管工具,但是合并带来的问题还是不能得到有效解决。
例如,CTO团体的DBA决定要对Employee数据库表结构进行修改,与此同时,COO团队的HR需要修改员工工时报表的格式。这样,就很可能出现来自不同团队的开发者分别对Employee类进行修改的情况,导致各自的修改互相冲突,这就必须要进行代码合并。该例子中,这次代码合并不仅有可能让CTO和COO各自要求的功能出错,甚至连CFO原本正常的功能也可能受到影响。
这样的案例有很多,它们的一个共同点是,多人为了不同的目的修改了同一份源代码,这很容易造成问题的产生,而避免这种问题产生的方法就是将服务不同行为者的代码进行切分。
解决方案
我们有很多方法可以解决上面例子中的问题,每一种方法都需要将相关的函数划分成不同的类。
1. 方法一
最简单直接的方法是将数据和函数分离。设计三个函数类共同使用一个数据类EmployeeData,每个函数类只包含与之行为者相关函数代码,这样就不存在互相依赖的情况了。如图:
这种方案的缺点在于:开发者需要在程序里处理三个函数类。
1. 方法二
在方法一的基础上,使用Facade设计模式(外观设计模式),把三个函数类中需要对外使用到的函数合并到一个Facade类中。如图:
Facade设计模式的好处是:EmployeeFacade类的代码量较少,它仅仅包含了初始化和调用三个具体实现类的函数。
当然,也有些开发者更倾向于把最重要的业务逻辑和数据放在一起。对于上面例子,我们可以选择将最重要的函数保留在Employee类中,同时用这个类来调用其它没那么重要的函数。如图:
为了清晰的了解这些解决方案,每个函数类中都只有一个函数,事实上并非如此,因为计算工资、生成报表和持久化数据都比较复杂的过程,每个函数类都可能包含了许多私有函数。总而言之,上面的每一个类都分别容纳了一组作用于相同作用域的函数,且每个作用域各自的私有函数是互相不可见的。
总结
单一职责原则主要讨论的是函数和类之间的关系——但是该原则在两个不同层面上会以不同的形式出现。在组件层面,可以将其称为共同闭包原则(Common Closure Principle),在软件架构层面,它是用于奠定架构边界的变更轴心(Axis of Change)。这两个原则会在后续的文章中深入讨论。