定义
一个类应该有且仅有一个能引起它变化的原因,否则类应该被拆分。
从定义可以看出,“职责”指导致类发生变化的原因,实质上就是项目的业务需求。定义只给出了类,方法和接口同样也可作为单一职责原则适用的对象,本文中以类作为举例,描述内容同样适用于方法、接口。
类对应的职责有改变的潜在区域。超过一个职责,意味着超过一个会发生改变的区域,会提高修改代码的风险。从词义角度出发,遵循单一职责原则似乎很简单,只需将项目划分为若干子模块,各自对应一个类即可。但在编程实践中,区分设计中的责任,也就是我们常说的对业务的抽象划分能力,恰恰是最困难的环节之一。更困难的是,即使在项目初期能够对其进行合理的功能拆分,由于无法避免出现职责扩散,原本遵循单一职责原则的软件实体变得不再遵循。因此,遵循单一职责原则要立足于当前需求,同时考虑未来的需求扩展。
职责扩散:软件需求发生变化等原因,职责P被分化为粒度更细的职责P1 和 P2.
作用
单一职责原则的核心是控制类的粒度大小,使其承担最小的责任,具备较高的内聚性。同时,减少了修改代码的可能性。
- 降低复杂度。职责单一,代码逻辑较多项职责简单。
- 提高可读性。复杂度降低,可读性必然提高。
- 提高可维护性。程序可读性高,维护更容易。
- 降低风险。这分为两个层次,其一是只有一个原因会导致修改代码动作,强调修改入口只有一个。其二是修改代码只会影响一个功能,不会影响其他功能。
实现方法
在两种情况下,要考虑遵循单一职责原则:
-
软件实体的设计阶段。这一阶段要求设计人员站在项目整体的高度,发现类的不同职责并将其分离,再封装到不同的类或模块中。由于软件实体较多且关系复杂,同时为了平衡设计模式的各个原则,该阶段易出现不遵循单一职责原则的软件实体。
-
需求变更导致职责扩散。此时可对原有软件实体进行重构,但最终是否分离旧类还需考虑多方面因素。
以上两种情况是对业务模型进行拆分,直至达到符合要求的粒度。一般来说,真是世界中的实体包含多想功能,譬如项目组长包含编码功能和管理功能,在完成拆分之后,需要利用继承或实现组合成符合业务的软件实体。因此,遵循单一职责原则总体上分为两步:
- 拆分。对职责进行分离。
- 组合。对职责进行组装。
在编码实践上,以下角度可使代码贴近单一职责原则
- 孤立变化
- 追踪依赖。检查类的构造从参数是否包含太多参数,因为每个参数都作为类的依赖存在,同时这些参数自身也拥有依赖,这样就形成了依赖链。可使用DI机制(依赖注入)动态的注入参数。
- 追踪方法参数。一般来讲,方法参数个数代表内部实现的职能,因此避免使用太多的方法参数。同时注意方法的命名,长的方法名称意味着内部存在复杂的逻辑。
- 尽早重构。发现可以重构时,果断进行微重构,使项目按照简洁的方式生长。
代码实践
正如上文的讨论,在设计类、接口、方法时都要遵循单一职责原则,否则应对其进行重构。
类层面
下图是不遵循单一职责的Bird类,可以看到存在明显的if-else语句:
public class Bird {
//程序中存在分支判断,这段代码可以拆分
public void moveMode(String birdName){
if("鸵鸟".equals(birdName)){
System.out.println(birdName + "用脚走路");
}else{
System.out.println(birdName + "用翅膀飞");
}
}
}
我们对其进行重构,以判断条件为分界,将该类拆分为两个类:
//类一,只包含翅膀分功能
public class FlyBird {
String birdName;
public FlyBird(String birdName){
this.birdName = birdName;
}
public void moveMode(){
System.out.println(birdName + "用翅膀飞");
}
}
//类二 ,只包含用脚走功能
public class FootBird {
String birdName;
public FootBird(String birdName){
this.birdName = birdName;
}
public void moveMode(){
System.out.println(birdName + "用脚走");
}
}
重构之后类图如下所示:
接口层面
同类层面类似,不遵循单一职责原则的接口如下
//接口包含了两项功能
public interface ICourse {
//获取课程名称
String getCourseName();
//管理课程
void managerCourse();
}
对上面接口进行拆分,得到两个接口如下
//定义获取课程名称的规范
public interface IGetName {
//获取课程名称
String getCourseName();
}
//定义管理课程的操作
public interface IManagerCourse {
//管理课程
void managerCourse();
}
同类不同的是,实现类可以继承多个接口,进行对功能进行组装,如下面代码
//实现两个接口,具备两个接口定义的功能
public class StudyCourse implements ICourse, IManagerCourse {
@Override
public String getCourseName() {
System.out.println("java课程");
return "java课程";
}
@Override
public void managerCourse() {
System.out.println("管理课程");
}
}
显然,这种方式对应的类图如下

方法层面
方法的情况同类和接口的情况类似,代码如下
public class Student {
//多个参数,不遵循单一职责原则
public void updataStudentInfo(String name, String age){
System.out.println(name + age);
}
//将上面方法拆分为下面两个方法
public void updateName(String name){
System.out.println(name);
}
public void updataAge(String age){
System.out.println(age);
}
//存在判断,可对其进行拆分
public void someAction(Boolean b){
if(b){
//do something
}else{
//do other thing
}
}
}
编程知识点
- 可变长度参数列表,其语法就是类型后跟…,表示此处接受的参数为0到多个Object类型的对象,或者是一个Object[]。使用可变长度参数列表时,
- 参数可以不传
- 可以传入单一的参数
- 可以传入多个参数
- 可以直接传入一个数组
参考资料
- 单一职责原则——面向对象设计原则
- 设计模式六大原则(1):单一职责原则
- Java 参数类型后面三个点,可变参数列表
- 读懂 SOLID 的「单一职责」原则
- Head First 设计模式(中文版)第九章 迭代器与组合模式