文章目录
- 定义
- 案例分析
- 重复的假象
- 代码合并
- 解决方案
- 小结
定义
SRP是SOLID五大设计原则中最容易被误解的一个。也许是名字的原因,很多程序员根据SRP这个名字想当然地认为这个原则就是指:每个模块都应该只做一件事。
在历史上,我们曾经这样描述SRP这一设计原则:
任何一个软件模块都应该有且仅有一个被修改的原因。
在现实环境中,软件系统为了满足用户和所有者的要求,必然要经常做出这样那样的修改。而该系统的用户或者所有者就是该设计原则中所指的“被修改的原因”。所以,我们也可以这样描述SRP:
任何一个软件模块都应该只对一个用户(User)或系统利益相关者(Stakeholder)负责。
不过,这里的“用户”和“系统利益相关者”在用词上也并不完全准确,它们很有可能指的是一个或多个用户和利益相关者,只要这些人希望对系统进行的变更是相似的,就可以归为一类——一个或多个有共同需求的人。在这里,我们将其称为行为者(actor)。
所以,对于SRP的最终描述就变成了:
任何一个软件模块都应该只对某一类行为者负责。
案例分析
重复的假象
某个工资管理程序中的Employee类有三个函数calculatePay()、reportHours()和save()
如上图所示这个类的三个函数分别对应的是三类非常不同的行为者,违反了SRP设计原则。
- calculatePay()函数是由财务部门制定的,他们负责向CFO汇报。
- reportHours()函数是由人力资源部门制定并使用的,他们负责向COO汇报。
- save()函数是由DBA制定的,他们负责向CTO汇报。
这三个函数被放在同一个源代码文件,即同一个Employee类中,程序员这样做实际上就等于使三类行为者的行为耦合在了一起,这有可能会导致CFO团队的命令影响到COO团队所依赖的功能。
例如,calculatePay()函数和reportHours()函数使用同样的逻辑来计算正常工作时数。程序员为了避免重复编码,通常会将该算法单独实现为一个名为regularHours()的函数:
接下来,假设CFO团队需要修改正常工作时数的计算方法,而COO带领的HR团队不需要这个修改,因为他们对数据的用法是不同的。这时候,负责这项修改的程序员会注意到calculatePay()函数调用了regularHours()函数,但可能不会注意到该函数会同时被reportHours()调用。
这类问题发生的根源就是因为我们将不同行为者所依赖的代码强凑到了一起。对此,SRP强调这类代码一定要被分开。
代码合并
一个拥有很多函数的源代码文件必然会经历很多次代码合并,该文件中的这些函数分别服务不同行为者的情况就更常见了。
例如,CTO团队的DBA决定要对Employee数据库表结构进行简单修改。与此同时,COO团队的HR需要修改工作时数报表的格式。
这样一来,就很可能出现两个来自不同团队的程序员分别对Employee类进行修改的情况。不出意外的话,他们各自的修改一定会互相冲突,这就必须要进行代码合并。
在这个例子中,这次代码合并不仅有可能让CTO和COO要求的功能出错,甚至连CFO原本正常的功能也可能受到影响。
多人为了不同的目的修改了同一份源代码,这很容易造成问题的产生。
解决方案
最简单直接的办法是将数据与函数分离,设计三个类共同使用一个不包括函数的、十分简单的EmployeeData类,每个类只包含与之相关的函数代码,互相不可见,这样就不存在互相依赖的情况了。
这种解决方案的坏处在于:程序员现在需要在程序里处理三个类。另一种解决办法是使用Facade设计模式:
这样一来,EmployeeFacade类所需要的代码量就很少了,它仅仅包含了初始化和调用三个具体实现类的函数。
我们也可以选择将最重要的函数保留在Employee类中,同时用这个类来调用其他没那么重要的函数:
总而言之,上面的每一个类都分别容纳了一组作用于相同作用域的函数,而在该作用域之外,它们各自的私有函数是互相不可见的。
小结
单一职责原则主要讨论的是函数和类之间的关系—但是它在两个讨论层面上会以不同的形式出现。在组件层面,我们可以将其称为共同闭包原则(Common Closure Principle),在软件架构层面,它则是用于奠定架构边界的变更轴心(Axis ofChange)。
参考内容来源于:《架构整洁之道》