引子
很多地方看过或听过单一责任原则,看了《The Single Responsibility Principle》这篇文章后,有了新的认识,翻译记录一下。原文中有些链接失效了,找了资源重新换了下。
翻译原文:The Single Responsibility Principle
正文
1972年,David L. Parnas 发表了一篇优秀论文,题目是《On the Criteria To Be Used in Decomposing Systems into Modules》。这篇文章出现在 12 月号的《Communications of the ACM》,第 15 卷,第 12 期。
在这篇论文中,Parnas 在一个简单的算法中,比较了两种不同的逻辑分解和分离策略。这篇论文读起来很有趣,强烈建议你研究一下。他的部分结论如下:
我们试图通过这些例子证明,根据流程图开始将系统分解为模块几乎总是不正确的。取而代之,我们建议从一系列困难的设计决策或可能改变的设计决策开始。然后,每个模块都设计成对其它模块隐藏这样的决策。
我在第二句加了重点强调。Parnas 的结论是,模块应该根据它们可能改变的方式进行分离,至少部分是这样。
两年后,Edsger Dijkstra 写了一篇题为《On the role of scientific thought》的优秀论文。在这篇文章中,他引入了一个术语:关注点的分离(The Separation of Concerns)。
20 世纪 70 年代和 80 年代是软件体系结构原则的盛行时期。结构化编程和设计(Structured Programming and Design)风靡一时。在这段时间里,Larry Constantine 引入耦合(Coupling)和内聚(Cohesion)的概念,Tom DeMarco、Meilir Page-Jones 和其他许多人都对其进行了扩展。
20 世纪 90 年代末,我试图将这些概念整合成一个原则,我称之为:单一责任原则(The Single Responsibility Principle)。(我有一种模糊的感觉,我从 Bertrand Meyer 那里偷了这个原则的名字,但我还没能证实这一点。)
单一责任原则(SRP)规定,每个软件模块都应该有且只有一个改变的理由。这听起来不错,似乎符合 Parnas 的理念。然而,它回避了一个问题:什么定义了改变的理由?
一些人想知道一个 bug 修复是否可以作为一个改变的理由,另一些人认为重构是否是改变的理由。这些问题可以通过指出“改变的理由””和“责任”之间的耦合来回答。
当然,这些代码对 bug 修复或重构并不负责。这些事情是程序员的责任,而不是程序的责任。但如果是这样的话,这个程序负责什么?或者,一个更好的问题是:这个程序对谁负责?更好的是:程序的设计必须回应谁?
想象一个典型的商业组织,高层有一位 CEO(首席执行官) ,向 CEO 报告的 C 级高管有:CFO(首席财务官)、COO(首席运营官)和 CTO(首席技术官)等。CFO 负责控制公司的财务。COO 负责管理公司的运营。CTO 负责公司内部的技术基础设施和开发。
现在思考下面的一段 Java 代码:
public class Employee {
public Money calculatePay();
public void save();
public String reportHours();
}
-
calculatePay
方法实现了一些算法,这些算法根据员工的合同、职位、工作时间等因素,来确定该员工应该得到多少报酬。 -
save
方法将Employee
对象管理的数据存储到企业数据库中。 -
reportHours
方法返回一个字符串,该字符串会添加到报表中,审计员使用该字符串来确保员工的工作时间合适,并获得合适的报酬。
现在,向 CEO 报告的 C 级高管中,谁负责规定 calculatePay
方法的行为?如果这个方法的规定出现严重错误,他们中的哪一个会被 CEO 解雇?显然,答案是 CFO 。规定雇员的工资是一项财务责任。如果因为 CFO 组织中有人规定了严重错误的计算薪酬规则,所有员工一年的薪酬都是双倍的,那么 CFO 很可能会被解雇。
另一个 C 级高管负责规定 reportHours
方法返回字符串的格式和内容。这个高管管理审计员和审核员,这是运营部门的职责。因此,如果该报告出现灾难性的错误规定,COO 将被解雇。
最后,如果 save
方法出现灾难性的错误规定,那么哪些 C 级主管将被解雇应该显而易见。如果企业数据库被如此可怕的错误规定所破坏,那么 CTO 很可能会被解雇。
因此,当 calculatePay
方法中的算法发生更改时,这些更改的要求将来自以 CFO 为首的组织,这是合理的。类似的,COO 的组织将要求更改 reportHours
方法,CTO 组织将要求更改 save
方法。
这就是单一责任原则的关键所在。这个原则是关于人的。
在编写软件模块时,你希望确保在请求变化时,这些变化只能来自单个人员,或者更确切地说,来自一个紧密耦合群体,这个群体代表一个严格定义的业务功能。你希望将你的模块,从整个组织的复杂性中隔离开来,并这样设计你的系统,使每个模块只负责(响应)一个业务功能的需求。
为什么?因为我们不想因为 CTO 的更改要求而导致解雇 COO 。对于我们的客户和经理来说,从他们的角度,发现一个程序的故障,与他们所要求的更改完全无关,没有什么比这更让他们害怕了。如果你更改 calculatePay
方法,并且无意中破坏了 reportHours
方法,那么 COO 将开始要求你再也不要更改 calculatePay
方法了。
想象一下,你把车开到了一个修理工那里,为了修理一扇坏了的电动车窗。他第二天打电话告诉你一切都修好了。当你提车时,你发现车窗运作正常,但车发动不起来。你不太可能回到那个机修工那里,因为他显然是个笨蛋。
这就是当客户和经理没有要求我们改变,我们破坏他们所关心的事情时的感受。
这就是我们不在 jsp 中使用 SQL 的原因。这就是我们不在计算结果的模块中生成 HTML 的原因。这就是业务规则不应该知道数据库模型的原因。这就是我们分离关注点的原因。
单一责任原则的另一个表达是:
将因同样原因而改变的事情集中起来。将因不同原因而改变的事情分开。
如果你思考一下,就会发现这只是定义内聚和耦合的另一种方式。我们希望增加因相同原因而改变的事物之间的内聚力,并减少因不同原因而改变的事物之间的耦合。
不管怎样,当你思考这个原则时,记住改变的原因是人。是人要求改变。不同人的关心出于不同的理由,你并不想把这些代码混合在一起,混淆那些人或者你自己。