第七章 单一职责原则(SRP)
在接下来的所有原则(SOLID)中,单一职责原则可能是比较难懂的,这很可能因为它的名字取得不准确,很容易让开发者从这个名字中认为它的含义是每个模块都只做一件事。
诚然,单一职责原则有点像类似的含义,一个函数有且只做一件事。我们用单一职责原则去重构一个繁杂庞大的函数成更小的函数,我们在最底的层面上使用它。但是,这并不是我们的SOLID原则中的单一职责原则!
传统上,单一职责原则是这么表述的:
一个模块有且只能由于一种原因被改变
软件系统发生改变是为了满足用户和利益相关者,这些用户和利益相关者是上述描述里“改变的原因”,那么,我们可以这么表述这项原则:
一个模块有且只能对其中之一负责,用户或者利益相关者
不幸的是,“用户”,“利益相关者”这些词在这里并不恰当,因为可能有多个用户想在一样的地方改变系统,利益相关者也是。然而,我们真的指代一个群体,要求修改的一个或更多的人,我们把这中群体称为角色。1
最终,我们这么描述单一职责原则:
一个模块有且只能对一个角色负责
现在,这里的“模块”我们指的是什么呢?最简单的定义就是源文件,最简单的定义就是源文件,大多时候这个定义都说的通。但在一些语言和开发环境,并不是用源文件去装代码。这种情况下一个模块是一组内聚的函数和数据结构。
“内聚”(cohesive)这个词暗示这SRP。内聚是一种绑定代码在一起对单一角色负责的力量。
也是最好的理解这个原则的方式就是去举例违反它的症状。
症状一:意外的重复
我最新的例子是工资应用里的Employee这个类。它右三个方法:calculatePay() , reportHours() , 和
save() (图7.1)。
图7.1 Employee类
这个类违反了SRP是因为这三个方法是对三个十分不同的角色负责的。
- calculatePay()方法描述了会计部门,他们对CFO报告。
- reportHours()方法描述了人力资源部门,他们对COO报告
- save()方法描述了数据库管理员(DBAs),他们对CTO报告
把这三个方法的源代码放在一个Employee类,开发者将使这三个角色相互耦合。这种耦合会造成CFO团队的动作将影响COO团队的所依赖的部分。
比如,设想calculatePay()函数和reportHours()函数公用了一个计算非工作日时间的公用算法。对于开发者并不希望出现重复代码,因此将这个算法封装成一个regularHours()函数(图7.2)。
图7.2 公用算法
现在假设CFO团队决定将非工作日的时间计算进行调整,相反,人力资源部门的COO团队对非工作日时间有不同的应用,并不希望调整。
开发者接到了这个变更任务,看到regularHours()函数被calculatePay()方法调用,不幸得是,开发者没有注意到这个函数同时也被reportHours函数调用。
开发者完成了变更开发也仔细地进行测试。CFO团队验收通过了,程序被部署了。
当然,COO团队并不知道这一切得发生,人力资源部门继续用了reportHours()函数生成得报告,但这数据已经不正确了。
最后问题被发现了,COO非常生气因为这些坏数据已经损失了百万美元得预算。
我们都看过类似得事情发生。这些问题得事发是因为我们将不同得角色依赖放在了十分接近得地方。SRP原则讲分割不同角色依赖的代码。
症状二:合并
并不难想象把包含不同方法的源文件合并到一起是很常见的。如果这些方法是对不同角色负责的,这种情况尤其可能发生。
比如,假设CTO团队的数据库管理员决定简单改变数据库里Employee表的定义(schema),再假设COO团队的HR职员决定改变报告里的时间格式。
两个不同的开发者,可能来自这两个不同的部门,看了Employee类,开始改造。不幸的是,这个改变冲突了,但是结果却合并了。
我可能都不需要告诉你合并是多风险的操作。现今我们的工具非常棒了,但并没有什么工具能处理任何合并的案子,这总是有风险的。
我们的例子中,合并CTO和COO的这两个变更是有风险的,可能造成给CFO的报告也不正确了。
我们还能发现很多其他的症状,但他们都涉及到多人由于不同原因变更了一样的源文件。
再强调,避免这种问题的方式:分割不同角色依赖的代码。
解决方法
这问题有很多不同的解决方法。每种都把这些函数移动不同的类中。
也许最显而易见的解决办法就是把数据从函数中分离出去,这三个类共用EmployeeData实例,这实例只是一个简单的数据结构,没有其他方法(图7.3)。每个类仅了了自己特殊的函数保存在源文件中,这三类不允许相互可见,这样意外重复就避免了。
图7.3 三个类互不可见
这种方案的缺点是开发者现在有三个类需要实例化和跟踪。解决这个困境的常见的办法是用门面模式。(图7.4)
图7.4 门面模式
EmployeeFacade类包含非常少的代码,它只负责实例化和委托访问后面的类和函数。
一些开发者倾向于把重要的业务规则写得离数据更近。我们可以这么做:把重要的方法保留到之前的Employee类里,然后用这个类作为门面访问更少的函数。(图7.5)
图7.5 把重要的方法保留到之前的Employee类里,然后用这个类作为门面访问更少的函数
你可能拒绝这些方案,因为你坚持每个类都应该之包含一个方法。对以上这个例子很难做到。函数的数量取决于计算费用,生成报表或保存数据这些方法,每个例子都可能有很多函数,每个类都应该会有很多的私有方法。
小结
单一职责原则是关于函数和类,但它出现在两个不同层面上的不同形式。组件层面上,它成了共同封闭原则,在架构层面上,它成了架构边界创建的改变轴2。这些在之后的章节里我们将会学习到。
1. Unfortunately, the words “user” and “stakeholder” aren’t really the right words to use here. There will likely be more than one user or stakeholder who wants the system changed in the same way. Instead, we’re really referring to a group—one or more people who require that change. We’ll refer to that group as an actor. ↩
2. At the architectural level, it becomes the Axis of Change responsible for the creation of Architectural Boundaries. ↩