Java 设计原则之单一职责原则(SRP)详解
主题来源:《设计模式之美》15-理论一:对于单一职责原则,如何判定某个类的职责是否够"单一"?
整理方向:面向 Java 后端面试与工程实践
一、背景:什么是 SOLID 原则
SOLID 并非一个单独的原则,而是由 5 个面向对象设计原则 组成,分别对应 S、O、L、I、D 五个字母:
| 字母 | 原则 | 英文 | 缩写 |
|---|---|---|---|
| S | 单一职责原则 | Single Responsibility Principle | SRP |
| O | 开闭原则 | Open Closed Principle | OCP |
| L | 里式替换原则 | Liskov Substitution Principle | LSP |
| I | 接口隔离原则 | Interface Segregation Principle | ISP |
| D | 依赖反转原则 | Dependency Inversion Principle | DIP |
本文重点讲解 SOLID 中的第一个原则:单一职责原则(SRP)。
这些设计原则从字面上看很容易"看懂",但真正用到项目中,你会发现"看懂"和"会用"是两回事,"用好"更是难上加难。很多人因为理解得不够透彻,在使用时过于教条主义,拿原则当真理,生搬硬套,适得其反。
二、如何理解单一职责原则(SRP)
1. 原则定义
英文原义:
A class or module should have a single responsibility.
中文翻译:
一个类或者模块只负责完成一个职责(或者功能)。
2. 两个关键概念:类 与 模块
关于"类"和"模块",有两种理解方式:
- 理解一:把模块看作比类更加抽象的概念,类也可以看作模块;
- 理解二:把模块看作比类更加粗粒度的代码块,模块中包含多个类,多个类组成一个模块。
无论哪种理解方式,单一职责原则应用到这两个对象时,道理都是相通的。本文主要从"类"设计的角度讲解,对于"模块"可自行引申。
3. 核心思想
- 不要设计大而全的类 ,要设计粒度小、功能单一的类;
- 一个类包含了两个或两个以上业务不相干的功能,就说明职责不够单一,应该拆分成多个功能更加单一、粒度更细的类。
4. 简单示例
假设一个类里既包含订单操作,又包含用户操作。订单和用户是两个独立的业务领域模型,将两个不相干的功能放到同一个类中,就违反了单一职责原则。
java
// 反例:违反 SRP
public class OrderAndUserService {
public void createOrder() { /* 订单逻辑 */ }
public void cancelOrder() { /* 订单逻辑 */ }
public User getUser(long userId) { /* 用户逻辑 */ }
public void updateUser(User user) { /* 用户逻辑 */ }
}
java
// 正例:拆分成两个职责单一的类
public class OrderService {
public void createOrder() { /* 订单逻辑 */ }
public void cancelOrder() { /* 订单逻辑 */ }
}
public class UserService {
public User getUser(long userId) { /* 用户逻辑 */ }
public void updateUser(User user) { /* 用户逻辑 */ }
}
三、如何判断类的职责是否足够单一?
1. 判断是主观的、依赖场景的
单一职责原则看似简单,但大部分情况下,类里的方法是归为同一类功能,还是归为不相关的两类功能,并不容易判定。
以社交产品中的 UserInfo 类为例:
java
public class UserInfo {
private long userId;
private String username;
private String email;
private String telephone;
private long createTime;
private long lastLoginTime;
private String avatarUrl;
private String provinceOfAddress; // 省
private String cityOfAddress; // 市
private String regionOfAddress; // 区
private String detailedAddress; // 详细地址
// ...省略其他属性和方法...
}
对于这个类是否满足单一职责,存在两种不同观点:
| 观点 | 理由 |
|---|---|
| 满足 SRP | 所有属性和方法都隶属于"用户"这样一个业务模型,包含的都是跟用户相关的信息 |
| 不满足 SRP | 地址信息占比重较高,可拆分为独立的 UserAddress 类,UserInfo 只保留其他信息,拆分后的两个类职责更单一 |
哪种观点更对?答案是:不能脱离具体的应用场景。
- 如果用户的地址信息与其他信息一样,只是单纯用来展示 ,那
UserInfo现在的设计就是合理的; - 如果产品后续加入了电商模块 ,地址信息会用于电商物流,那就最好将地址信息从
UserInfo中拆分出来,独立成物流/地址/收货信息类; - 如果公司发展壮大,支持统一账号系统 (一个账号可在所有产品中登录),还需要继续拆分,将
email、telephone等身份认证信息抽取成独立的类。
2. 判定结论
不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,判定结果可能都不一样。
- 在某种应用场景/当下需求下,一个类可能已经满足 SRP;
- 换个场景或到了未来某个需求背景下,可能就不满足了,需要继续拆分。
例如从不同业务层面看 UserInfo:
- 从"用户"层面看 → 信息都属于用户,满足职责单一;
- 从"用户展示信息""地址信息""登录认证信息"等更细粒度业务层面看 → 应该继续拆分。
3. 不要过度设计:持续重构
既然 SRP 的判定是主观的、仁者见仁智者见智的,在实际开发中:
我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,再将这个粗粒度的类拆分成几个更细粒度的类。这就是所谓的"持续重构"。
不要过于未雨绸缪、过度设计。
四、侧面的判定指标(更具可执行性)
与其主观地思考类是否职责单一,以下几条判断原则更有指导意义、更具可执行性:
| 序号 | 判定指标 | 说明 |
|---|---|---|
| 1 | 类中代码行数、函数或属性过多 | 影响代码的可读性和可维护性,需考虑拆分 |
| 2 | 类依赖的其他类过多,或被依赖的类过多 | 不符合高内聚、低耦合的设计思想,需考虑拆分 |
| 3 | 私有方法过多 | 考虑能否将私有方法独立到新类中设为 public,供更多类复用,提高复用性 |
| 4 | 很难给类起一个合适的名字 | 只能笼统地命名为 Manager、Context 之类,说明职责定义不清晰 |
| 5 | 类中大量方法集中操作某几个属性 | 例如 UserInfo 中一半方法都在操作 address 信息,可考虑拆分出这几个属性和对应方法 |
关于"多少算多"的量化问题
初级工程师常问:多少行代码才算"行数过多"?多少个函数、属性才叫"过多"?
这个问题难以定量回答,就像问大厨"放盐少许"中的"少许"是多少,大厨很难给出具体量值。
- 对编程初学者,可以给出一个宽泛可量化的参考标准 :
- 一个类的代码行数 最好不超过 200 行;
- 函数个数、属性个数 最好都不超过 10 个。
- 从感受上讲,当出现这些"坏味道"时,往往就说明需要拆分了:
- 读代码让你头大了;
- 实现某个功能时不知道该用哪个函数;
- 想用某个函数翻半天找不到;
- 只用到一个小功能,却要引入整个类(类中包含很多无关此功能的函数)。
写多了项目、代码写多了,在开发中慢慢"品尝",自然就知道什么是"放盐少许"了------这就是所谓的**"专业第六感"**。
五、类的职责是否设计得越单一越好?
答案是否定的。 拆分过细会适得其反,反而降低内聚性和可维护性。
示例:Serialization 类的拆分
原始 Serialization 类实现了一个简单协议的序列化和反序列化功能:
java
/**
* Protocol format: identifier-string;{gson string}
* For example: UEUEUE;{"a":"A","b":"B"}
*/
public class Serialization {
private static final String IDENTIFIER_STRING = "UEUEUE;";
private Gson gson;
public Serialization() {
this.gson = new Gson();
}
public String serialize(Map<String, String> object) {
StringBuilder textBuilder = new StringBuilder();
textBuilder.append(IDENTIFIER_STRING);
textBuilder.append(gson.toJson(object));
return textBuilder.toString();
}
public Map<String, String> deserialize(String text) {
if (!text.startsWith(IDENTIFIER_STRING)) {
return Collections.emptyMap();
}
String gsonStr = text.substring(IDENTIFIER_STRING.length());
return gson.fromJson(gsonStr, Map.class);
}
}
为了职责更单一,将其拆分为 Serializer 和 Deserializer 两个类:
java
public class Serializer {
private static final String IDENTIFIER_STRING = "UEUEUE;";
private Gson gson;
public Serializer() {
this.gson = new Gson();
}
public String serialize(Map<String, String> object) {
StringBuilder textBuilder = new StringBuilder();
textBuilder.append(IDENTIFIER_STRING);
textBuilder.append(gson.toJson(object));
return textBuilder.toString();
}
}
public class Deserializer {
private static final String IDENTIFIER_STRING = "UEUEUE;";
private Gson gson;
public Deserializer() {
this.gson = new Gson();
}
public Map<String, String> deserialize(String text) {
if (!text.startsWith(IDENTIFIER_STRING)) {
return Collections.emptyMap();
}
String gsonStr = text.substring(IDENTIFIER_STRING.length());
return gson.fromJson(gsonStr, Map.class);
}
}
拆分过细带来的问题
拆分后职责确实更单一了,但随之而来新问题:
- 内聚性变差 :如果修改协议格式(数据标识从
UEUEUE改为DFDFDF)或序列化方式(从 JSON 改为 XML),Serializer和Deserializer两个类都需要修改,内聚性反而不如原来的Serialization; - 可维护性变差 :如果只修改了
Serializer而忘记修改Deserializer,会导致序列化、反序列化不匹配,程序运行出错。
核心观点 :不管是应用设计原则还是设计模式,最终目的都是提高代码的可读性、可扩展性、复用性、可维护性。在考虑应用某个设计原则是否合理时,也应该以此作为最终的考量标准。
六、重点回顾
1. 如何理解单一职责原则(SRP)?
- 一个类只负责完成一个职责或者功能;
- 不要设计大而全的类,要设计粒度小、功能单一的类;
- 单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。
2. 如何判断类的职责是否足够单一?
不同的应用场景、不同阶段的需求背景、不同的业务层面,判定结果可能不同。一些侧面的判断指标更具指导意义和可执行性:
- 类中的代码行数、函数或属性过多;
- 类依赖的其他类过多,或者依赖类的其他类过多;
- 私有方法过多;
- 比较难给类起一个合适的名字;
- 类中大量的方法都是集中操作类中的某几个属性。
3. 类的职责是否设计得越单一越好?
- SRP 通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性;
- 类职责单一,类依赖的和被依赖的其他类也会变少,减少耦合,实现高内聚、低耦合;
- 但拆分过细会适得其反,反倒降低内聚性,影响可维护性。
七、单一职责原则的延伸应用
SRP 不仅适用于类的设计,还可延伸到更多设计方面:
| 延伸层次 | 应用方式 | 实例 |
|---|---|---|
| 方法(函数)设计 | 每个方法只做一件事,代码多了乱了就应拆分 | 自上而下的编程方式,先定义核心方法再写细节 |
| 接口设计 | 一个接口只负责某一个模块的事情 | 不要将多个模块的功能写在一个接口里 |
| 组件/框架设计 | 让组件职责单一,避免臃肿 | Android 的 Activity 过于臃肿,MVP、MVVM 框架让其职责单一 |
| 子系统/架构设计 | 划分模块、子系统、微服务 | 微服务拆分本身就是 SRP 的一种实践 |
| 技术选型 | 让专业的软件做专业的事 | 数据库专注存取,Redis 专注缓存,消息队列专注异步解耦 |
八、面试要点速记
- SRP 定义:一个类/模块只负责完成一个职责(功能)。
- 目的:高内聚、低耦合,提升复用性、可读性、可维护性。
- 核心思想:不要大而全,要小而专。
- 判定标准是主观的:依赖应用场景、需求阶段、业务层面,没有统一量化标准。
- 实操策略 :先写粗粒度类满足业务,随业务发展通过持续重构拆分,不过度设计。
- 侧面判定指标:代码行数/方法/属性过多、依赖过多、私有方法过多、难以命名、方法集中操作某几个属性。
- 量化参考:类代码行数 ≤ 200,函数个数、属性个数 ≤ 10(宽泛参考,非硬性要求)。
- 不要拆分过细:拆分过细会降低内聚性和可维护性,应用设计原则最终标准是代码质量(可读性、可扩展性、复用性、可维护性)。
- 延伸应用:方法、接口、组件、模块、子系统、微服务、技术选型。
参考资料:极客时间《设计模式之美》第 15 篇------理论一:对于单一职责原则,如何判定某个类的职责是否够"单一"?