Single Responsibility Principle (SRP) - OO设计的单一职责原则
There should never be more than one reason for a class to change.
永远不要让一个类存在多个改变的理由。
换句话说,如果一个类需要改变,改变它的理由永远只有一个。如果存在多个改变它的理由,就需要重新设计该类。
SRP(Single Responsibility Principle)原则的核心含意是:只能让一个类有且仅有一个职责。这也是单一职责原则的命名含义。
为什么一个类不能有多于一个以上的职责呢?
如果一个类具有一个以上的职责,那么就会有多个不同的原因引起该类变化,而这种变化将影响到该类不同职责的使用者(不同用户):
1,一方面,如果一个职责使用了外部类库,则使用另外一个职责的用户却也不得不包含这个未被使用的外部类库。
2,另一方面,某个用户由于某个原因需要修改其中一个职责,另外一个职责的用户也将受到影响,他将不得不重新编译和配置。
这违反了设计的开闭原则,也不是我们所期望的。
既然一个类不能有多个职责,那么怎么划分职责呢?
Robert.C Martin给出了一个著名的定义:所谓一个类的一个职责是指引起该类变化的一个原因。
If you can think of more than one motive for changing a class,then that class has more than one responsibility.
如果你能想到一个类存在多个使其改变的原因,那么这个类就存在多个职责。
Single Responsibility Principle (SRP)的原文里举了一个Modem的例子来说明怎么样进行职责的划分,这里我们也沿用这个例子来说明一下:
SRP违反例:
Modem.java
interface Modem {
public void dial(String pno);//拨号
public void hangup();//挂断
public void send(char c);//发送数据
public char recv();//接收数据
}
咋一看,这是一个没有任何问题的接口设计。但事实上,这个接口包含了2个职责:第一个是连接管理(dial,hangup);另一个是数据通信(send,recv)。很多情况下,这2个职责没有任何共通的部分,它们因为不同的理由而改变,被不同部分的程序调用。
所以它违反了SRP原则。
下面的类图将它的2个不同职责分成2个不同的接口,这样至少可以让客户端应用程序使用具有单一职责的接口:
让ModemImplementation实现这两个接口。我们注意到,ModemImplementation又组合了2个职责,这不是我们希望的,但有时这又是必须的。通常由于某些原因,迫使我们不得不绑定多个职责到一个类中,但我们至少可以通过接口的分割来分离应用程序关心的概念。
事实上,这个例子一个更好的设计应该是这样的,如图:
面向对象设计五大原则的理解,他们分别是:SRP——单一职责原则;OCP——开放封闭原则;LSP——Liskov替换原则;DIP——依赖倒置原则;ISP——接口隔离原则。
1. 单一职责原则
在《敏捷软件开发》中,把“职责”定义为“变化的原因”,也就是说,就一个类而言,应该只有一个引起它变化的原因。
在《UML与模式应用》一书中又提到,“职责”可以定义为“一个类或者类型的契约或者义务”,并把职责分成“知道”型职责和“做”型职责。
其中“做”型职责指的是一个对象自己完成某种动作或者和其他对象协同完成某个动作;“知道”型职责指的是一个对象需要了解哪些信息。如果按照这种方式来定义“职责”的话,就与《敏》中对单一职责原则的定义不太相符了,所以还是理解为“变化的原因”比较恰当。
这个原则很好理解,但是既然谈到了职责,就不妨再来看看GRASP——通用职责分配软件模式(选自《UML与模式应用》)。按照我自己的看法来讲,在下面这些职责分配模式中所涉及到的设计问题,是建立在现实世界抽象层次上的设计,从这个层次上进一步细化,才到了设计模式所说的针对接口编程和优先使用组合的两大原则。
在这个层次上的抽象,一定要按照现实生活中的思维方法来进行,从我们人类考虑问题的角度出发,把解决现实问题的思维方式逐渐转化成程序能够理解的思维方式,绝不允许在这一步考虑程序代码如何实现,那样子的架构就是基于程序实现逻辑,而不是从解决问题的角度出发来实现业务逻辑(参考“面向对象的思维方法”)。
1) 专家模式。
在一个系统中可能存在成千上万个职责,在面向对象的设计中,定义对象的交互时,就要做出如何将职责分配给类的设计选择。
专家模式的解决方案就是:把一个职责分配给信息专家——掌握了为履行职责所必需的信息的类。
按照专家模式可以得到:一个对象所执行的操作通常是这个对象在现实世界中所代表的事物所执行的操作——这恰恰印证了我上面中的说法。
不过使用专家模式的时候,一定要仔细判断什么样的职责是应该只由一个类完成,什么样的职责应该由不同的类协作完成。举一个小小的反例吧,在“思维方法”一文中,提供了一个收发邮件的例子用以说明作者的观点,源码如下所示:
public class JunkMail {
private String head;
private String body;
private String address;
public JunkMain() { // 默认的类构造器
this.head=...;
this.body=...;
}
public static boolean sendMail(String address) {
// 调用qmail,发送email
}
public static Collection listAllMail() {
}
}
作者在这里就犯了一个职责分配的错误:上面的head、body和address都是属于邮件自身的属性,但是这个类却有一个叫做sendMail的方法,错误就在这个方法这里。在现实生活中,我们发送邮件的时候,是通过邮递员来进行的,绝对没有一封信会长上翅膀自己飞到收信人的手中,在程序中也是一样,一封邮件绝不可能自己把自己发送出去,应该通过某个MailController之类的类来完成这个功能(之所以不命名为MailSender,是因为后面可能还要添加receiveMail等功能)。
2) 创建者模式
如果下列条件满足的话,就把创建类A的实例的职责分配给类B的实例:
a) B聚集了A对象
b) B包含了A对象
c) B记录了A对象的实例
d) B要经常使用A对象
e) 当A的实例被创建时,B具有要传递给A的初始化数据(也就是说B是创建A的信息专家)
如果以上条件中不止一条满足的话,那么最好让B聚集或者包含A
创建者模式用于指导对象实例创建任务的分配,基本目的就是找到一个与被创建对象有关联关系的创建者。
3) 低耦合度
4) 高聚合度