1972年,David L.Parnas发表了一篇经典论文,题目是关于将系统分解为模块时使用的标准。它出现在12月份的ACM通讯上,第15卷,第12期。
文中,Parnas比较了在一个简单算法中分解和分离逻辑的两种不同策略。这篇论文读起来很吸引人,我强烈要求你去研究它。他的结论部分如下:
“我们试图通过这些例子证明,根据流程图开始将系统分解为模块几乎总是不正确的。相反,我们建议从一系列困难的设计决策或可能发生变化的设计决策开始。然后,每个模块都被设计为隐藏此类决策其他人。”
我在第二句到最后一句增加了强调。Parnas的结论是,模块应该根据它们可能改变的方式分开,至少部分分开。
两年后,Edsger Dijkstra写了另一篇题为科学思想的作用的经典论文。他在书中引入了一个术语:关注点分离。
20世纪70年代和80年代是软件体系结构原则的肥沃时期。结构化编程和设计风靡一时。在此期间,耦合和内聚的概念由Larry Constantine提出,Tom DeMarco、Meilir Page-Jones和其他许多人进一步阐述。
在20世纪90年代末,我试图将这些概念整合成一个原则,我称之为:单一责任原则。(我有一种模糊的感觉,我从Bertrand Meyer那里盗取了这一原则的名称,但我无法证实。)
单一责任原则(SRP)规定,每个软件模块都应该有一个且只有一个更改原因。这听起来不错,似乎符合Parnas的说法。然而,它回避了一个问题:什么定义了改变的理由?
一些人想知道一个bug修复是否可以作为一个改变的理由。其他人怀疑重构是否是改变的原因。这些问题可以通过指出“改变的原因”和“责任”之间的耦合来回答。
当然,代码不负责bug修复或重构。这些事情是程序员的责任,而不是程序的责任。但如果是这样的话,该项目负责什么?或者,也许更好的问题是:该计划对谁负责?更好的是:该计划的设计必须对谁做出反应?
想象一个典型的商业组织。高层有一位首席执行官。向首席执行官汇报的是C级主管:首席财务官、首席运营官和首席技术官等。首席财务官负责控制公司的财务状况。首席运营官负责管理公司的运营。CTO负责公司内部的技术基础设施和开发。
现在考虑一下java代码:
public class Employee {
public Money calculatePay();
public void save();
public String reportHours();
}
- calculatePay方法实现了基于特定员工的合同、状态、工作时间等确定该员工应支付多少工资的算法。
- “save”方法将Employee对象管理的数据存储到企业数据库中。
- reportHours方法返回一个字符串,该字符串附加到报告中,审计员使用该字符串来确保员工工作的小时数适当,并获得适当的报酬。
现在,向首席执行官报告的C级主管中,哪一位负责指定calculatePay方法的行为?如果这种方法被灾难性地错误指定,他们中的哪一位会被CEO解雇?显然,答案是首席财务官。规定员工薪酬是一项财务责任。如果所有员工都因为首席财务官组织中的某个人错误地指定了计算薪酬的规则而获得一年双薪,首席财务官很可能会被解雇。
不同的C级执行人员负责指定reportHours方法返回的字符串的格式和内容。该主管负责管理审计员和审查员,这是运营部的职责。因此,如果该报告出现灾难性的错误说明,首席运营官将被解雇。
最后,如果save方法出现灾难性的错误规范,那么哪些C级主管将被解雇应该是显而易见的。如果企业数据库被如此可怕的错误规范破坏,CTO可能会被解雇。
因此,当在calculatePay方法中对算法进行更改时,这些更改的请求将来*首席财务官领导的组织,这是理所当然的。同样,COO的组织将请求更改reportHours方法,CTOs组织将请求更改save方法。
这就是单一责任原则的关键所在。这个原则是关于人的。
在编写软件模块时,您希望确保在请求更改时,这些更改只能来自单个人员,或者更确切地说,来自表示单个狭义定义的业务功能的单个紧密耦合的人员组。您希望将您的模块从整个组织的复杂性中分离出来,并设计您的系统,使每个模块只负责(响应)该业务功能的需求。
为什么?因为我们不想让首席运营官被解雇,因为我们做了首席技术官要求的改变。在我们的客户和经理看来,没有什么比发现一个程序以一种与他们要求的更改完全无关的方式出现故障更让他们害怕的了。如果您更改了calculatePay方法,并且无意中中断了reportHours方法;然后首席运营官将开始要求您不再更改calculatePay方法。
想象一下,为了修理一扇坏了的电动车窗,你把车交给了一个机械师。他第二天打电话给你说一切都修好了。当你提车时,你会发现车窗运转良好;但是汽车发动不了。你不太可能回到那个机修工那里,因为他显然是个白痴。
这就是当我们打破了客户和经理所关心的、他们没有要求我们改变的事情时的感受。
这就是我们不将SQL放在JSP中的原因。这就是我们不在计算结果的模块中生成HTML的原因。这就是业务规则不应该知道数据库模式的原因。这就是我们分开关注的原因。
单一责任原则的另一个措辞是:
把因同样原因而发生变化的事物聚集在一起。将因不同原因而改变的事物分开。
如果你仔细想想,你会发现这只是定义内聚和耦合的另一种方式。我们希望增加因相同原因而改变的事物之间的内聚力,我们希望减少因不同原因而改变的事物之间的耦合。
然而,当你思考这个原则时,请记住,改变的原因是人。是人们要求改变。你也不想混淆那些人,或者你自己,把许多不同的人出于不同的原因所关心的代码混在一起。
[1] 我在这里有点夸张。C级主管通常不会因为小的错误规格而被解雇。尽管如此,这并不是不可能的,它确实强调了向这些高管报告的组织关注不同的问题。