SRP:单一职责原则 一个类应该只有一个发生变化的原因。
为何把两个职责分离到单独的类中很重要呢?因为每一个职责都有变化的一个轴线。当需求变化时,该变化会反映为类的职责的变化。如果一个类承担了多于一个的职责,那么引起它变化的原因就会有多个。
如果一个类承担的职责过多,就等于把这些职责耦合在了一起。一个职责发生变化可能会削弱或抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏。
有两个不同的应用程序使用Rectangle类。一个应用程序是有关计算几何学方面的,利用Rectangle类计算几何形状,但不会再屏幕上绘制矩形。另外一个应用程序实质上是有关图形绘制方面的,它可能也会进行一些计算几何方面的工作,但是它肯定会在屏幕上绘制矩形。
这个设计违反了单一职责原则(SRP)。Rectangle类具有两个职责。第一个职责提供了一个矩形几何形状的数学模型;第二个职责是把矩形在一个图形用户界面上绘制出来。
对于SRP的违反导致了一些严重的问题。首先,我们必须在计算几何应用程序中包含进GUI代码。在.Net中,就必须要把GUI组建和计算几何应用一起构建、部署。
其次,如果GraphicalApplication的改变由于一些原因导致了Rectangle的改变,那么这个改变会迫使我们重新构建、测试以及部署ComputationalGeometryApplication。如果忘记这样做,ComputationalGeometryApplication可能会以不可预测的方式失败。
一个较好的设计是把这两个职责分离到两个完全不同的类中。这个设计把Rectangle类中进行计算的部分移动到了GeometricRectangle类中。现在矩形绘制方式的改变不会对ComputationalGeometryApplication类造成影响。
定义职责
在SRP中,我们把职责定义为变化的原因。如果你能够想到多于一个的动机去改变一个类,那么这个类就具有多于一个的职责。有时,我们很难注意到这一点。我们习惯于以组的形式去考虑职责。
public interface Modem { public void Dial(string pno); public void Hangup(); public void Send(char c); public char Recv(); }
这个接口显示出两个职责。第一个职责是连接管理;第二个职责是数据通信。Dial和Hangup函数进行调制解调器的连接处理,而Send和Recv函数进行数据通信。
这两个职责应该分开吗?这依赖于应用程序变化的方式。如果应用程序的变化会影响到连接函数的签名(signature),那么这个设计就具有僵化性的臭味,因为调用Send和Recv的类必须要重新编译、部署的次数常常会超过我们希望的次数。在这种情况下,这两个职责应该被分离。这样做避免了客户应用程序和这两个职责耦合在一起。
另一方面,如果应用程序的变化方式总是导致这两个职责同时变化,那么就不必分离它们,实际上,分类它们就会具有不必要的复杂性的臭味。
在此还有一个推论。仅当变化发生时,变化的轴线才具有实际意义。如果没有征兆,那么应用SRP或者任何其他原则都是不明智的。
分离耦合的职责
请注意,上面把两个职责都耦合进了Modem类中。这不是所希望的,但是或许是必要的。常常会有一些和硬件或者操作系统的细节有关的原因,迫使我们把不愿耦合在一起的东西耦合在了一起。然而,对于应用程序其他部分来说,通过分类它们的接口我们已经解耦了概念。
我们可以把Modem类看作是一个杂凑物,或者有缺陷的类。然后,请注意所有的以来关系都是从它发出的。谁都不需要依赖于它。除了main外,谁也不需要知道它的存在。因此,我们已经把丑陋的部分隐藏起来了。其丑陋性不会泄漏出来,污染应用程序的其他部分。
结论
SRP是所有原则中最简单的原则之一,也是最难正确运用的原则之一。我们会自然地把职责结合在一起。软件设计真正要做的许多工作,就是发现职责并把那些职责相互分离。
摘录自:[美]RobertC.Martin、MicahMartin著,邓辉、孙鸣译 敏捷软件开发原则、模式与实践(C#版修订版) [M]、人民邮电出版社,2013、89-92、