Java单一职责原则SRP详解

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 中拆分出来,独立成物流/地址/收货信息类;
  • 如果公司发展壮大,支持统一账号系统 (一个账号可在所有产品中登录),还需要继续拆分,将 emailtelephone身份认证信息抽取成独立的类。

2. 判定结论

不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,判定结果可能都不一样。

  • 在某种应用场景/当下需求下,一个类可能已经满足 SRP;
  • 换个场景或到了未来某个需求背景下,可能就不满足了,需要继续拆分。

例如从不同业务层面看 UserInfo

  • 从"用户"层面看 → 信息都属于用户,满足职责单一;
  • 从"用户展示信息""地址信息""登录认证信息"等更细粒度业务层面看 → 应该继续拆分。

3. 不要过度设计:持续重构

既然 SRP 的判定是主观的、仁者见仁智者见智的,在实际开发中:

我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展,如果粗粒度的类越来越庞大,代码越来越多,再将这个粗粒度的类拆分成几个更细粒度的类。这就是所谓的"持续重构"。

不要过于未雨绸缪、过度设计。


四、侧面的判定指标(更具可执行性)

与其主观地思考类是否职责单一,以下几条判断原则更有指导意义、更具可执行性

序号 判定指标 说明
1 类中代码行数、函数或属性过多 影响代码的可读性和可维护性,需考虑拆分
2 类依赖的其他类过多,或被依赖的类过多 不符合高内聚、低耦合的设计思想,需考虑拆分
3 私有方法过多 考虑能否将私有方法独立到新类中设为 public,供更多类复用,提高复用性
4 很难给类起一个合适的名字 只能笼统地命名为 ManagerContext 之类,说明职责定义不清晰
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);
    }
}

为了职责更单一,将其拆分为 SerializerDeserializer 两个类:

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);
    }
}

拆分过细带来的问题

拆分后职责确实更单一了,但随之而来新问题:

  1. 内聚性变差 :如果修改协议格式(数据标识从 UEUEUE 改为 DFDFDF)或序列化方式(从 JSON 改为 XML),SerializerDeserializer 两个类都需要修改,内聚性反而不如原来的 Serialization
  2. 可维护性变差 :如果只修改了 Serializer 而忘记修改 Deserializer,会导致序列化、反序列化不匹配,程序运行出错

核心观点 :不管是应用设计原则还是设计模式,最终目的都是提高代码的可读性、可扩展性、复用性、可维护性。在考虑应用某个设计原则是否合理时,也应该以此作为最终的考量标准。


六、重点回顾

1. 如何理解单一职责原则(SRP)?

  • 一个类只负责完成一个职责或者功能;
  • 不要设计大而全的类,要设计粒度小、功能单一的类;
  • 单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。

2. 如何判断类的职责是否足够单一?

不同的应用场景、不同阶段的需求背景、不同的业务层面,判定结果可能不同。一些侧面的判断指标更具指导意义和可执行性:

  • 类中的代码行数、函数或属性过多;
  • 类依赖的其他类过多,或者依赖类的其他类过多;
  • 私有方法过多;
  • 比较难给类起一个合适的名字;
  • 类中大量的方法都是集中操作类中的某几个属性。

3. 类的职责是否设计得越单一越好?

  • SRP 通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性;
  • 类职责单一,类依赖的和被依赖的其他类也会变少,减少耦合,实现高内聚、低耦合
  • 拆分过细会适得其反,反倒降低内聚性,影响可维护性。

七、单一职责原则的延伸应用

SRP 不仅适用于类的设计,还可延伸到更多设计方面:

延伸层次 应用方式 实例
方法(函数)设计 每个方法只做一件事,代码多了乱了就应拆分 自上而下的编程方式,先定义核心方法再写细节
接口设计 一个接口只负责某一个模块的事情 不要将多个模块的功能写在一个接口里
组件/框架设计 让组件职责单一,避免臃肿 Android 的 Activity 过于臃肿,MVP、MVVM 框架让其职责单一
子系统/架构设计 划分模块、子系统、微服务 微服务拆分本身就是 SRP 的一种实践
技术选型 让专业的软件做专业的事 数据库专注存取,Redis 专注缓存,消息队列专注异步解耦

八、面试要点速记

  1. SRP 定义:一个类/模块只负责完成一个职责(功能)。
  2. 目的:高内聚、低耦合,提升复用性、可读性、可维护性。
  3. 核心思想:不要大而全,要小而专。
  4. 判定标准是主观的:依赖应用场景、需求阶段、业务层面,没有统一量化标准。
  5. 实操策略 :先写粗粒度类满足业务,随业务发展通过持续重构拆分,不过度设计。
  6. 侧面判定指标:代码行数/方法/属性过多、依赖过多、私有方法过多、难以命名、方法集中操作某几个属性。
  7. 量化参考:类代码行数 ≤ 200,函数个数、属性个数 ≤ 10(宽泛参考,非硬性要求)。
  8. 不要拆分过细:拆分过细会降低内聚性和可维护性,应用设计原则最终标准是代码质量(可读性、可扩展性、复用性、可维护性)。
  9. 延伸应用:方法、接口、组件、模块、子系统、微服务、技术选型。

参考资料:极客时间《设计模式之美》第 15 篇------理论一:对于单一职责原则,如何判定某个类的职责是否够"单一"?

相关推荐
探数API小喇叭18 分钟前
基站查询 API 怎么用?LAC、CELLID 参数详解与实战】
java·开发语言·api·基站定位
catino19 分钟前
spring-IOC、DI
java·spring·rpc
Mojitocean24 分钟前
Win开发环境配置(持续更新)
开发语言·python
千里码aicood32 分钟前
flask基于数字人的储粮知识问答原型系统研究与实现
后端·python·flask
E_ICEBLUE39 分钟前
Python 实现 Markdown 转 Word、PDF:从转换到页面设置
python·pdf·word·markdown·格式转换
哒咩哒咩12942 分钟前
Agent 智能体开发全攻略:从 ReAct 到企业级架构
python·langchain·fastapi
xcl09251 小时前
宠物救助领养系统实战开发指南:从需求分析到部署上线
java·mfc·宠物