Skip to main content
  1. Posts/

OODP0 单一职责原则(SRP)

·79 words·1 min·
Eric Linus
Author
Eric Linus
北京邮电大学软件工程专业本科生,主要语言C++,对系统编程,数据库和AI系统交叉感兴趣。熟悉C++/Python/C#/Java/Rust。Github:@n00bme0w
Table of Contents

一个类应该仅有一个引起它变化的原因
#

实践上我遇到的问题是如何定义引起它变化的原因?是宏观的原因还是微观的原因?

答案是宏观的
#

Eric: 单一职责原则实践过程怎么把握,比如课程中说你要做一个Journal类来存入和查看日记,如果你又想要持久化它的话,不能直接在Journal类中写Save方法,而是要写一个新类Persistence来管理持久化。依据是一个类只能有一个引起他变化的原因。但是Journal类本身不也有存入和查看两个功能吗,两个功能变化时都要修改Journal类啊,这符合单一职责原则吗?难道要把存入和查看两个功能也分成两个类吗?具体实践中我们又要怎么把握呢?

Deepseek的回答
#

1. 重新理解“引起变化的原因”
#

单一职责原则(SRP)的核心不是“一个类只能有一个方法”,而是“一个类应该有且仅有一个引起它变化的原因”。

这原因不是一个具体的功能,而是一个**高层级的、会变化的“轴”**或“关切点”。

  • Journal类的职责:管理日记的核心业务逻辑(业务实体)。

    • 变化原因:当“日记”这个概念本身需要变化时,这个类才需要改变。
    • 例子:我们需要为日记增加“标签”功能;我们需要修改日记的标题长度限制;我们需要在日记内容中支持Markdown格式。这些变化都围绕着“日记是什么、有什么行为”这个核心关切点。
    • “添加条目”和“查看条目” 都是对日记这个核心实体进行操作的核心行为。它们共享同一个变化原因——日记业务逻辑的变更。因此,它们属于同一个职责。
  • Persistence类的职责:处理数据的持久化机制(存储与读取)。

    • 变化原因:当数据存储的方式或位置发生变化时,这个类才需要改变。
    • 例子:我们之前把日记保存到文本文件,现在想改成保存到MySQL数据库;我们想从本地存储改为云存储(如AWS S3);我们想改变文件保存的格式(从JSON改为XML)。这些变化完全不影响Journal类本身是什么,它只关心“怎么存、存到哪里”。

2. 为什么不能把Save方法放在Journal里?
#

如果将Save方法直接放在Journal类中,就意味着这个类现在有两个引起它变化的原因:

  1. 日记的业务逻辑变化(如增加标签)。
  2. 持久化机制的变化(如从文件存到数据库)。

这违反了SRP。带来的直接问题是:

  • 可维护性差:一个负责数据库连接的开发人员修改了Save方法,可能会意外影响到另一个只关心业务逻辑(如addEntry方法)的开发人员。
  • 复用性差:如果你想重用Journal这个业务实体,但新的项目想用不同的存储方式(比如内存存储),你就必须把Journal类也拖过去,并附带一堆你可能不需要的文件I/O代码。
  • 测试困难:测试Journal的业务逻辑时,你必须绕过或模拟(Mock)文件系统操作,使得单元测试变得复杂。

而通过引入Persistence类,你将变化隔离了。Journal的变更和Persistence的变更互不影响。你可以轻松地替换不同的持久化策略而不影响核心业务代码。

3. 实践中如何把握这个“度”?(关键部分)
#

这是一个设计权衡的艺术,没有绝对正确的答案,但有一些非常实用的指导原则:

a. 关注“变化轴”,而非功能数量
#

不要简单地数一个类有多少个方法。而要问自己:“哪些变化会迫使这个类被修改?”。如果所有方法都因为同一个宏观原因而变化,那它们很可能属于同一个职责。

  • Journal的例子addEntryremoveEntry都围绕着“管理条目生命周期”这个轴。
  • 另一个例子:一个User类有loginlogoutchangePassword方法。它们都围绕着“用户身份认证”这个轴。但如果把“发送营销邮件sendPromotionalEmail”也塞进User类,那就不对了,因为那属于“通知”或“营销”这个不同的变化轴。

b. 应用“共同闭包原则”(CCP)
#

这是包级别的一个原则,但同样适用于类设计:将那些同时、因同样原因而变化的东西放在一起

  • Journal里的方法总是因为日记业务的变化而一起改变。
  • Persistence类里的方法总是因为存储技术的变化而一起改变。
  • 但它们两者之间几乎不会因为对方的原因而改变。所以它们应该被分开。

c. 从“角色”和“用户”视角思考
#

想象一下谁会要求这个类做出改变。

  • 产品经理可能会要求改变Journal:“我们需要给日记加个心情图标”。
  • 运维工程师可能会要求改变Persistence:“数据库要从MySQL迁移到PostgreSQL,连接字符串和方言要改”。

如果来自完全不同角色、不同关切方的需求会迫使你修改同一个类,那么这个类很可能承担了多个职责。

d. 避免过早优化和过度设计
#

在项目早期,如果持久化方式非常稳定(比如确定就用一个简单的JSON文件),并且项目很小,那么稍微违反一下SRP的代价可能低于引入一个新类的复杂度。一开始可以放在一起,但要有意识地将实现细节隐藏好(比如用private方法),并在第一次需要变化时(比如要加第二种存储方式)果断进行重构,将其提取到独立的类中。

总结
#

  • Journal类的“存入”和“查看”:属于对同一业务实体(日记)的操作,是同一个职责,变化原因相同,因此放在一个类是合理的,符合SRP。
  • Journal类的“Save”方法:属于另一个职责(持久化),变化原因不同(业务逻辑 vs. 存储技术),因此应该拆分成Persistence类。
  • 实践心法
    1. 聚焦于 “变化的原因” ,而不是方法的数量。
    2. 思考 “谁会要求它改变”(产品经理 vs. 运维)。
    3. 遵循 “共同闭包原则”——因同样原因而变化的东西放在一起。
    4. 循序渐进,在简单和设计之间权衡,在代码第一次发出“臭味”(难以修改、难以测试)时果断重构。