陈述:
就一个类而言,应该只有一个导致其变化的原因
分析:一个职责就是一个变化的轴线。
一个类如果承担的职责过多,就等于将这些职责耦合在一起。一个职责的变化可能会虚弱或者抑止这个类完成其它职责的能力。
多职责将导致脆弱性的臭味。
示例1:
Rectangle类具有两个职责:
计算矩形面积的数学模型
将矩形在一个图形设备上描述出来
Rectangle类违反了SRP,具有两个职能——计算面积和绘制矩形
这种对SRP的违反将导致两个方面的问题:
包含不必要的代码
一个应用可能希望使用Retangle类计算矩形的面积,但是却被迫将绘制矩形相关的代码也包含进来
一些逻辑上毫无关联的原因可能导致应用失败
如果GraphicalApplication的需求发生了变化,从而对Rectangle类进行了修改。但是这样的变化居然会要求我们重新构建、测试以及部署ComputationalGeometryApplication,否则其将莫名其妙的失败。
修改后的设计如下:
示例2:
一个Modem的接口:
- ClassModem{
- public:
- virtualvoiddail(char*pno)=0;
- virtualvoidhangup()=0;
- virtualvoidsend(charc)=0;
- virtualvoidrecv()=0;
- };
Modem类(可能)有两个职责:
- 拨号
- 通信
--什么是职责?
职责是“变化的原因”。
职责是“变化的原因”。
--上面的例子可能存在两种变化的方式:
- 连接和通信可能独立变化
- 连接和通信同时变化
修改后的设计如下:
有一点需要注意:在ModemImplementation中实际还是集合了两个职责。这是我们不希望的,但是有时候却是必须的。
但是我们注意到,对于应用的其它部分,通过接口的分离我们已经实现了职责的分离。
ModemImplementation已经不被其它任何程序所依赖。除了main以外,其他所有程序都不需要知道这个函数的存在。
但是我们注意到,对于应用的其它部分,通过接口的分离我们已经实现了职责的分离。
ModemImplementation已经不被其它任何程序所依赖。除了main以外,其他所有程序都不需要知道这个函数的存在。
质疑:
Q:ModemImplementation有两个原因引起变化?应采用如下类图。
A:我们面向接口编程,对外公布的是接口而非实现类。如果真要实现类的单一职责,就必须使用组合模式,会引起类间耦合过重、类的数量增加等问题,人为增加设计的复杂性。
Q:ModemImplementation有两个原因引起变化?应采用如下类图。
A:我们面向接口编程,对外公布的是接口而非实现类。如果真要实现类的单一职责,就必须使用组合模式,会引起类间耦合过重、类的数量增加等问题,人为增加设计的复杂性。
单一职责原则的好处:
- 类的复杂性降低,实现什么职责都有清晰明确的定义;
- 可读性提高,复杂性降低,可维性提高;
- 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影
常见错误提醒:
- 持久化与业务规则的耦合。
Employee类包含了业务规则和对持久化的控制。
业务规则经常变化,而持久化方法却一般不变,而且变化原因也完全不同。将这两个职责耦合在一起,将导致每次因为业务规则变化调整Employee类时,所有持久化部分的代码也要跟着变化
业务规则经常变化,而持久化方法却一般不变,而且变化原因也完全不同。将这两个职责耦合在一起,将导致每次因为业务规则变化调整Employee类时,所有持久化部分的代码也要跟着变化
小结:
- 如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏。
- 我们会自然地把职责结合在一起。如果能想到多于一个动机去改变一个类,那么这个类就具有多于一个的职责,就应该考虑类的职责分离。
- 软件设计真正要做的许多内容,就是发现职责并把那些职责相互分离。
- SRP是所有原则中最简单、也是最难运用的。其余原则会以这样或那样的方式回到这个问题上。
- 单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口类或类设计得是否好,但“职责”和“变化原因”都是
不可度量的,因项目而异,依环境而异。 - 单一职责原则很难在项目中得到体现。考虑工期、成本、人员技术水平、硬件情况甚至项目之外的因素。
- 建议:接口一定做到单一职责,类的设计尽量做到只有一个原因引起变化。