一、定义
一个类应该只有一个发生变化的原因。
二、为什么要使用SRC
因为每一个职责都是变化的一个轴线。当需求变化时,这种变化就会反映为类的职责的变化。如果一个类承担了多于一个的职责,那么引起它变化的原因就会有多个。
如果一个类承担的职责过多,就等于把这些职责耦合在了一起。一个职责的变化可能会消弱或抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭到意想不到的破坏。
三、案例演示
考虑图中的设计。
Rectangle类具有两个方法,一个方法把矩形绘制到屏幕上,另一个方法则是计算矩形的面积。 现在有两个应用程序使用Rectangle类。一个是利用Rectangle计算矩形面积,但不需要绘制矩形;另一个应用程序可能会绘制矩形,也可能计算面积。
这个设计违反了单一职责原则。Rectangle类具有两个职责。第一个职责提供了矩形面积的计算;第二个职责提供了矩形绘制。 对于SRC的违反导致了一些严重的问题。
对于这个违反了SRC的程序有以下几点问题:
首先,我们必须在左侧的程序中包含进GUI代码,即使我们是不需要的。
其次,如果右侧程序的改变导致了Rectangle的改变,那么这个改变会迫使我们重新构建、测试和部署左侧的程序。
一个较好的设计就是把这两个职责分离到如下图所示的两个完全不同的类中。这个设计把Rectangle类中进行计算的部分移到了GeometricRectangle类中。现在矩形绘制方法的变化不会影响到左侧的程序。
四、什么是职责
在SRC中,我们把职责定义为变化的原因。
如果你能够想到多于一个的动机去改变一个类,那么这个类就具有多于一个的职责。
有时,我们很难注意到这一点,我们习惯于以组的形式去考虑职责。 例如,下面的Modem接口,大多数人认为这个接口非常合理。该接口所声明的4个方法确实是调制解调器所具有的功能 。
public interface Modem
{
public void Dial(string pno); //连接
public void Hangup(); //断开
public void Send(char c); //发送
public char Recv(); //接收
}
然而,该接口中却显示出两个职责。第一个职责是连接管理;第二个职责是数据通信。
这两个职责需要分开吗?
这依赖于应用程序变化的方式。如果应用程序的变化会影响到连接管理方法的签名,那么这个设计就具有僵化性,因为调用send和recv的类必须重新编译、部署。在这种情况下,这两个职责应该被分离,如下图所示。
另一方面,如果应用程序的变化方式总是导致这两个职责同时变化,那就不必分离它们。
记住这么一个结论,仅当变化发生时,变化的轴线才具有实际意义。如果没有征兆,那么应用SRP或者任何其它原则都是不明智的。
五、分离耦合的职责
请注意,在上图中,把两个职责都耦合进了ModemImplementation类中。这也许不是最好的,但是或许必须得这么做。常常会有一些和硬件或者操作系统的细节有关的原因,迫使我们把不愿意耦合在一起的东西耦合在了一起。然而,对于应用的其余部分来说,通过分离它们的接口我们已经解耦了。
六、持久化
下图展示了一种常见的违反SRP的情形。
Employee类包含了业务规则和对于持久化的控制。这个两个职责在大多数情况下绝不应该混合在一起。业务规则往往会频繁地变化,而持久化的方式却不会如此频繁的变化,并且变化的原因也是完全不同的,它们实在两个方向上变化,一个是业务方向上变化,另一个是持久化方向上变化。
把业务规则和持久化子系统绑定在一起的做法是自讨苦吃。 测试驱动的开发实践常常会远在设计出现臭味之前就迫使我们分离这两个职责。然而,如果测试没有迫使这种分离,那么就应该使用Facade(外观)、DAO(数据访问)或者Proxy(代理)模式对设计进行重构,分离这两个职责。
七、结论
SRP是所有原则中最简单的原则之一,也是最难正确运用的原则之一。我们会不自觉地把职责结合到一起。软件设计真正要做的许多工作,就是发现职责并把那些职责相互分离。