一个类应该仅有一个引起它变化的原因#
实践上我遇到的问题是如何定义引起它变化的原因?是宏观的原因还是微观的原因?
答案是宏观的#
Eric: 单一职责原则实践过程怎么把握,比如课程中说你要做一个Journal类来存入和查看日记,如果你又想要持久化它的话,不能直接在Journal类中写Save方法,而是要写一个新类Persistence来管理持久化。依据是一个类只能有一个引起他变化的原因。但是Journal类本身不也有存入和查看两个功能吗,两个功能变化时都要修改Journal类啊,这符合单一职责原则吗?难道要把存入和查看两个功能也分成两个类吗?具体实践中我们又要怎么把握呢?
Deepseek的回答#
1. 重新理解“引起变化的原因”#
单一职责原则(SRP)的核心不是“一个类只能有一个方法”,而是“一个类应该有且仅有一个引起它变化的原因”。
这原因不是一个具体的功能,而是一个**高层级的、会变化的“轴”**或“关切点”。
Journal类的职责:管理日记的核心业务逻辑(业务实体)。
- 变化原因:当“日记”这个概念本身需要变化时,这个类才需要改变。
- 例子:我们需要为日记增加“标签”功能;我们需要修改日记的标题长度限制;我们需要在日记内容中支持Markdown格式。这些变化都围绕着“日记是什么、有什么行为”这个核心关切点。
- “添加条目”和“查看条目” 都是对日记这个核心实体进行操作的核心行为。它们共享同一个变化原因——日记业务逻辑的变更。因此,它们属于同一个职责。
Persistence类的职责:处理数据的持久化机制(存储与读取)。
- 变化原因:当数据存储的方式或位置发生变化时,这个类才需要改变。
- 例子:我们之前把日记保存到文本文件,现在想改成保存到MySQL数据库;我们想从本地存储改为云存储(如AWS S3);我们想改变文件保存的格式(从JSON改为XML)。这些变化完全不影响
Journal类本身是什么,它只关心“怎么存、存到哪里”。
2. 为什么不能把Save方法放在Journal里?#
如果将Save方法直接放在Journal类中,就意味着这个类现在有两个引起它变化的原因:
- 日记的业务逻辑变化(如增加标签)。
- 持久化机制的变化(如从文件存到数据库)。
这违反了SRP。带来的直接问题是:
- 可维护性差:一个负责数据库连接的开发人员修改了
Save方法,可能会意外影响到另一个只关心业务逻辑(如addEntry方法)的开发人员。 - 复用性差:如果你想重用
Journal这个业务实体,但新的项目想用不同的存储方式(比如内存存储),你就必须把Journal类也拖过去,并附带一堆你可能不需要的文件I/O代码。 - 测试困难:测试
Journal的业务逻辑时,你必须绕过或模拟(Mock)文件系统操作,使得单元测试变得复杂。
而通过引入Persistence类,你将变化隔离了。Journal的变更和Persistence的变更互不影响。你可以轻松地替换不同的持久化策略而不影响核心业务代码。
3. 实践中如何把握这个“度”?(关键部分)#
这是一个设计权衡的艺术,没有绝对正确的答案,但有一些非常实用的指导原则:
a. 关注“变化轴”,而非功能数量#
不要简单地数一个类有多少个方法。而要问自己:“哪些变化会迫使这个类被修改?”。如果所有方法都因为同一个宏观原因而变化,那它们很可能属于同一个职责。
- Journal的例子:
addEntry和removeEntry都围绕着“管理条目生命周期”这个轴。 - 另一个例子:一个
User类有login、logout、changePassword方法。它们都围绕着“用户身份认证”这个轴。但如果把“发送营销邮件sendPromotionalEmail”也塞进User类,那就不对了,因为那属于“通知”或“营销”这个不同的变化轴。
b. 应用“共同闭包原则”(CCP)#
这是包级别的一个原则,但同样适用于类设计:将那些同时、因同样原因而变化的东西放在一起。
Journal里的方法总是因为日记业务的变化而一起改变。Persistence类里的方法总是因为存储技术的变化而一起改变。- 但它们两者之间几乎不会因为对方的原因而改变。所以它们应该被分开。
c. 从“角色”和“用户”视角思考#
想象一下谁会要求这个类做出改变。
- 产品经理可能会要求改变
Journal:“我们需要给日记加个心情图标”。 - 运维工程师可能会要求改变
Persistence:“数据库要从MySQL迁移到PostgreSQL,连接字符串和方言要改”。
如果来自完全不同角色、不同关切方的需求会迫使你修改同一个类,那么这个类很可能承担了多个职责。
d. 避免过早优化和过度设计#
在项目早期,如果持久化方式非常稳定(比如确定就用一个简单的JSON文件),并且项目很小,那么稍微违反一下SRP的代价可能低于引入一个新类的复杂度。一开始可以放在一起,但要有意识地将实现细节隐藏好(比如用private方法),并在第一次需要变化时(比如要加第二种存储方式)果断进行重构,将其提取到独立的类中。
总结#
- Journal类的“存入”和“查看”:属于对同一业务实体(日记)的操作,是同一个职责,变化原因相同,因此放在一个类是合理的,符合SRP。
- Journal类的“Save”方法:属于另一个职责(持久化),变化原因不同(业务逻辑 vs. 存储技术),因此应该拆分成
Persistence类。 - 实践心法:
- 聚焦于 “变化的原因” ,而不是方法的数量。
- 思考 “谁会要求它改变”(产品经理 vs. 运维)。
- 遵循 “共同闭包原则”——因同样原因而变化的东西放在一起。
- 循序渐进,在简单和设计之间权衡,在代码第一次发出“臭味”(难以修改、难以测试)时果断重构。
