单一职责原则也是面向对象设计原则中的一条,下面我们就来详细地对其进行介绍。
单一职责原则的定义
单一职责原则(Single Responsibility Principle,SRP)又称单一功能原则,由罗伯特·C.马丁(Robert C. Martin)于《敏捷软件开发:原则、模式和实践》一书中提出的。这里的职责是指类变化的原因,单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分(There should never be more than one reason for a class to change)。
该原则提出对象不应该承担太多职责,如果一个对象承担了太多的职责,至少存在以下两个缺点:
- 一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力;
- 当客户端需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成冗余代码或代码的浪费。
单一职责原则的优点
单一职责原则的核心就是控制类的粒度大小、将对象解耦、提高其内聚性。如果遵循单一职责原则将有以下优点。
- 降低类的复杂度。一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多。
- 提高类的可读性。复杂性降低,自然其可读性会提高。
- 提高系统的可维护性。可读性提高,那自然更容易维护了。
- 变更引起的风险降低。变更是必然的,如果单一职责原则遵守得好,当修改一个功能时,可以显著降低对其他功能的影响。
单一职责原则的实现方法
单一职责原则是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,再封装到不同的类或模块中。而发现类的多重职责需要设计人员具有较强的分析设计能力和相关重构经验。下面以大学学生工作管理程序为例介绍单一职责原则的应用。
【例1】大学学生工作管理程序。
分析:大学学生工作主要包括学生生活辅导和学生学业指导两个方面的工作,其中生活辅导主要包括班委建设、出勤统计、心理辅导、费用催缴、班级管理等工作,学业指导主要包括专业引导、学习辅导、科研指导、学习总结等工作。如果将这些工作交给一位老师负责显然不合理,正确的做 法是生活辅导由辅导员负责,学业指导由学业导师负责,其类图如图 1 所示。
图1 大学学生工作管理程序的类图
注意:单一职责同样也适用于方法。一个方法应该尽可能做好一件事情。如果一个方法处理的事情太多,其颗粒度会变得很粗,不利于重用。
进阶阅读
如果您想了解单一职责原则在实际项目中的应用,可以猛击阅读《使用单一职责原则解决实际问题》文章。
使用单一职责原则解决实际问题
本节介绍如何使用单一职责原则解决问题。
例如,在 C语言中文网中,用户分为普通用户和VIP用户。普通用户不能阅读学习收费文章,VIP用户可以任意反复阅读,功能职责不一样。
首先创建用户类 User。
package net.biancheng.c.srp;
public class User {
public void study(String userType) {
if ("common".equals(userType)) {
System.out.println("不能阅读收费文章");
} else {
System.out.println("可以任意反复阅读收费文章");
}
}
}
下面编写客户端测试代码,无论是普通用户还是VIP会员,都调用 study() 方法的逻辑。
public static void main(String[] args) {
User user = new User();
user.study("common");
user.study("vip");
}
从上面代码看,User 类承担了两种处理逻辑。假如现在对用户进行加密,那么普通用户和VIP会员的加密逻辑是不一样的,必须修改代码。而修改代码逻辑势必会相互影响,容易带来不可控的风险。
下面我们对职责进行分离解耦,分别创建 CommonUser 和 VipUser。
CommonUser 类的代码如下。
package net.biancheng.c.srp;
public class CommonUser {
public void study(String userType) {
System.out.println(userType + "不能阅读收费文章");
}
}
VipUser 类的代码如下。
package net.biancheng.c.srp;
public class VipUser {
public void study(String userType) {
System.out.println(userType + "任意阅读收费文章");
}
}
客户端代码如下,将普通会员的处理逻辑调用 CommonUser 类,VIP 会员的处理逻辑调用 VipUser 类。
public static void main(String[] args) {
CommonUser commonUser = new CommonUser();
commonUser.study("common");
VipUser vipUser = new VipUser();
vipUser.study("vip");
}
对于教程方面,我们要对教程做权限。普通学员只能获得教程的基本信息,VIP 用户可以获得阅读权限。所以在控制教程层面,至少有两个职责,我们可以把展示职责和管理职责分离开,都实现同一个抽象依赖。
设计一个顶层接口 Course。
package net.biancheng.c.srp;
public interface Course {
//获取教程的基本信息
String getCourseTitle();
//获得阅读权限
byte[] getReadPow();
//学习教程
void studyCourse();
//退款
void refundCourse();
}
这里可以把 Course 接口拆分成两个接口,创建一个接口 CourseInfo 和 CourseManager。
CourseInfo 接口的代码如下。
package net.biancheng.c.srp;
public interface CourseInfo {
//获取教程的基本信息
String getCourseTitle();
//获得阅读权限
byte[] getReadPow();
}
CourseManager 接口的代码如下。
package net.biancheng.c.srp;
public interface CourseManager {
//学习教程
void studyCourse();
//退款
void refundCourse();
}
类图如下:
下面来看方法层面的单一职责设计。有时候,我们通常会把一个方法写成下面这样。
private void modifyUserInfo(String userName, String url) {
userName = "C语言中文网";
url = "http://c.biancheng.net/";
}
或者是这样。
private void modifyUserInfo(String userName, String pwd, String url) {
userName = "C语言中文网";
pwd = "123456";
url = "http://c.biancheng.net/";
}
显然上面两种写法的 modifyUserInfo() 方法都承担了多个职责,既可以修改 userName,也可以修改 url,甚至更多,但是这样设计明显不符合单一职责原则,我们做出如下修改,把这个方法拆成两个。
private void modifyUserName(String userName) {
userName = "C语言中文网"
}
private void modifyUrl(String url) {
url = "http://c.biancheng.net/"
}
代码在修改之后,开发和维护都会变得更加简单和容易。在实际项目中,代码会存在依赖、组合、聚合关系,在项目开发过程中还会受到项目的规模、周期、技术人员水平、对进度把控的影响,导致很多类都不能满足单一职责原则。但是,我们在编写代码的过程中,应尽可能地让接口和方法保持单一职责,这样对项目后期的维护是有很大帮助的。