什么是单一职责原则(Single Responsibility Principle, SRP)
在讲解什么是单一职责原则之前,我们先说一个例子,吊一下口味:我们在做项目的时候,会接触到用户,机构,角色管理这些模块,基本上使用的都是RBAC模型(Role-Based Access Control,基于角色的访问控制, 通过分配和取消角色来完成用户权限的授予和取消,使动作主体(用户)与资源的行为(权限)分离)。现在假设这样一种场景,我们把用户管理,修改用户信息,增加机构,增加角色等维护信息写到一个接口中进行管理,类图如下:
分析上面的类图我们会发现,这样的设计是非常不合理的,用户的属性和用户的行为是两种不同的业务模式,把它们都写在一个类中显然不行。我们应该把用户的信息抽取成一个BO(Business Object, 业务对象), 把行为抽取成一个Biz(Business Logic, 业务逻辑), 重新设计的类图如下:
SRP在类或接口中的使用
SRP的原话是:There should never be more than one reason for a class to change.翻译过来其实也很好懂:应该有且仅有一个原因引起类的变更。看下面的例子:
/**
上面的的类图对应的接口入下
*/
public interface IPhone{
//拨通电话
public void dial(String phoneNumber);
//通话
public void chat(Object o);
//挂断电话
public void hangup();
}
在看到这个接口的时候,我们都会认为这样的设计是没有问题的,拨通电话,通话,挂断电话写在同一个接口里面并没有什么错。但是,我们仔细分析,这个接口真的没有问题吗?单一职责原则要求一个接口或类只有一个原因引起变化,也就是说一个接口或一个类只有一个原则,它就只负责一件事。 但我们分析上面这个接口,却发现它包含了两个职责:一个时协议管理,一个是数据传送。dial()和hangup()两个方法实现的是协议管理,分别是拨通电话和挂机。chat()实现的是数据传送,把我们说的话转换成模拟信号或数字信号传递给对方,然后再把对方传递过来的信号还原成我们听得懂的语言。这里的协议接通和数据传送的变化都会引起该接口或实现类的变化。我们想一想,这两个职责会相互影响吗?不管是什么协议,协议接通只负责将电话接通就行,而数据传输只需要传输数据,不必要去管协议是如何接通的。所以通过分析,IPhone接口包含了两个职责,而且这两个职责的变化不互相影响,这就可以考虑分成两个接口,类图如下:
观察上面的类图,我们发现这样的设计会比原来笼统的设计优雅的多,现在的设计在职责上比原来更加分明,让人一眼就能看出这个接口负责的是什么。也许有人会问,Phone这个类实现了两个接口,又把两个职责融合在了一个类中,那么是不是就有两个原因引起了它的变化了呢?别忘了,我们是面向接口编程,我们对外公布的是接口(API),并非实现类,给你提供了模板,在接口层面已经为你明确了职责,那么具体的实现怎么弄就需要开发者去考虑了。
SRP也适用于方法
其实,单一职责原则不仅适用于类,接口,同样适用于方法中。这要举一个例子了,比如我们做项目的时候会遇到修改用户信息这样的功能模块,我们一般的想法是将用户的所有数据都接收过来,比如用户名,信息,密码,家庭地址等等,然后统一封装到一个User对象中提交到数据库,我们一般都是这么干的,就如下面这样:
其实这样的方法是不可取的,因为职责不明确,方法不明确,你到底是要修改密码,还是修改用户名,还是修改地址,还是都要修改?这样职责不明确的话在与其他项目成员沟通的时候会产生很多麻烦,正确的设计如下:
SRP的优点
- 类的复杂性降低,对于实现什么职责都有清晰明确的定义。
- 可读性提高。
- 可维护性提高。
- 变更引起的风险降低,一个接口的修改只对相应的实现类有影响,对其他接口无影响,这对系统的扩展性,维护性都有非常大的帮助。
参考书籍
- 《设计模式之禅》