引子

很多地方看过或听过单一责任原则,看了《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 的原因。这就是业务规则不应该知道数据库模型的原因。这就是我们分离关注点的原因。

单一责任原则的另一个表达是:

将因同样原因而改变的事情集中起来。将因不同原因而改变的事情分开。

如果你思考一下,就会发现这只是定义内聚和耦合的另一种方式。我们希望增加因相同原因而改变的事物之间的内聚力,并减少因不同原因而改变的事物之间的耦合。

不管怎样,当你思考这个原则时,记住改变的原因是。是要求改变。不同人的关心出于不同的理由,你并不想把这些代码混合在一起,混淆那些人或者你自己。

参考资料


XXHolic
363 声望1.1k 粉丝

[链接]