原文地址:The Single Responsibility Principle
在1972年,David L. Parnas发表了一篇题为《On the Criteria To Be Used in Decomposing Systems into Modules》的经典论文。它被刊登在ACM通讯杂志第12期第15卷上。
在这篇论文中,Parnas采用了一种简单的方法,比较了两种不同的分解逻辑的策略。这篇论文非常值得一读,我强烈建议你好好阅读一下。他的部分结论如下:
我们试图通过这些例子来证明,根据流程图把一个系统分解成模块几乎总是不正确的。相反,我们建议从一个设计很复杂或设计很有可能会变化的地方开始。每个模块都被设计为只会被一种变化因素影响。
Parnas的结论是,在某种程度上,模块的拆分要基于将来可能的变化因素。
两年后,Edsger Dijkstra写了同样堪称经典的一篇论文《 On the role of scientific thought》。在这篇论中他提出了关注点分离(Separation of Concerns)的概念。
二十世纪七八十年代是软件架构思想繁盛时期。结构化编程和设计非常流行。在那段时间,Larry Constantine提出了耦合和内聚的概念,后由Tom DeMarco, Meilir Page-Jones和其他人加以扩充完善。
在九十年代后期,我试图将这些概念合并成一个原则。我把这个原则称为单一职责原则(Single Responsibility Principle)。 (我有这种模糊的感觉,我从Bertrand Meyer那里盗用了的这个原则的名字,但我还没有证实这一点。)
单一职责原则(SRP)指出,每个软件模块应该有且只有一个理由去改变。这听起来很不错,似乎与Parnas想法一致。然而它回避了问题的本质:改变的理由究竟是指什么?这里的理由如何定义?
一些人想知道bug修复是否可以作为改变的理由。另外一些人好奇重构是否是改变的理由。这些问题可以通过指出“改变理由”和“责任”这两个术语之间的联系来回答。
显然代码的职责不是bug修复或者重构。这些应该是程序员的责任,而不是程序的。但如果是这样的话,程序负责什么?或者说:这个程序是谁负责?谁必需对程序的设计负责?
想象一个典型的商业组织。一般情况下,公司有一个首席执行官(CEO),而需要向首席执行官汇报工作的直接责任人有:首席财务官(CFO),首席运营官(COO),首席技术官(CTO)。首席财务官负责控制公司的财务状况。首席运营官负责管理公司的日常运营。首席技术官是负责公司技术基础设施和发展。
现在看看下面JAVA程序段:
Public class Employee {
public Money calculatePay();
public void save();
public String reportHours();
}
- calculatePay方法根据员工的合同,状态,工作时间等情况计算支付给员工的薪水
- save方法把员工相关数据存入公司数据库
- reportHours方法返回员工的工作情况报告,审计人员可以确保员工工作了足够的时间并付给员工相对应的报酬
现在,上面提到的COO,CFO和CTO中的哪一个需要对calculatePay
方法的的行为负责?也就是说,如果这个方法出了问题,哪一个会被CEO开除呢?很明显,答案是CFO。发放工资是财务的责任。如果CFO手下的某个员工把计算薪水的规则弄错了,从而导致所有的员工都被发放了双倍的薪水,CFO将难辞其咎,多半会被开除吧。
这里应该还有一个首席官对reportHours
的格式和内容负责。他领导审计人员,掌握公司运作情况,报告给CEO。如果出具的报告混乱不堪,错误百出,面临被开除的是谁?没错,就是COO。
最后,如果save
方法出了灾难性的行为,哪一个首席官会被开除就很明显了。如果公司的数据库崩溃了,数据丢失,那么CTO将被开除。
因此可以看出,calculatePay
方法被修改的需求来自CFO领导的团队。同样的,COO和他领导的团队会对reportHours
方法有修改需求,而CTO团队极有可能提出对save
方法的修改。
这就是单一职责的关键。它是关于人的。
当你写一个软件模块的时候,你应该确保,当需求变更的时候,需求的来源应该是一个人,更准确的说,应该是对某个业务功能负责的人群。你应该把复杂的系统拆分成独立的模块,使得每一个模块都只满足一个业务功能的需求。
为什么要这样?
因为,我们不希望由于我们为CTO的需求做了一些修改导致COO被开除。最让我们的客户感到不能理解的是出现了一个完全与他们要求的改变无关的程序故障,至少在他们看来是没有任何关联的。如果你在改变calculatePay
方法的时候无意中修改了reportHours
,那么COO可能会要求你永远都不要去碰calculatePay
方法了。
想象一下,你把车开到修理厂,找了一个修理工来修理你的汽车车窗。第二天他告诉你已经修理好了。当你去取车的时候,你发现车窗已经修好了但是车子却打不燃火了。我想你再也不会找这个修理工修车了。
当你改动了客户在意的功能但是客户根本就没叫你去修改的时候,客户就是这个感觉。
这就是我们不能把SQL直接写到JSP中,不把生成HTML的代码和负责数据计算的代码放在一起,业务逻辑不应该知道数据库架构的原因。这也是我们关注分离点(separate concerns)的理由。
单一职责的另一种描述是:
会被同样的原因改变的事情聚集在一起。把会被多种原因改变的事情分离开。
如果你仔细回味上面的描述你就会意识到,这只是另外一种定义内聚和耦合的方式。对于相同原因引起的变化,我们希望提高内聚,而对于会被多种原因改变的事情我们希望降低耦合。
不管怎样,当你脑海中浮现这个法则的时候,请记住变化的原因是人。是人带来的变化。如果你不想让其他人或者你自己感到困惑,那么请不要把不同的人因为不同的原因关心的代码放在一起。