单一职责原则(SRP)

共 17 题
📑 题目列表 17 题
#
★★★

1. SRP 的 Robert Martin 原意中变化原因(reason to change)唯一,而非职责单一

请解释 Robert Martin 提出的单一职责原则(SRP)的原始含义,并说明它与"职责单一"的通俗理解有何区别?

  • 对 SRP 原始定义("一个类只有一个变化原因")的准确理解
  • 区分"职责"与"变化原因"两个概念的不同
  • 理解 SRP 为什么是"变化驱动"而非"功能驱动"的

Robert Martin 在《敏捷软件开发:原则、模式与实践》中给出的 SRP 定义是"一个模块应该只有一个引起它变化的原因(reason to change)"。注意这里的核心概念是"变化原因"而不是"职责"。通俗理解的"职责单一"容易让人误以为"一个类只做一件事",但更精确的表述是:当你因为某种业务需求的变化而需要修改这个类时,不应该有第二个不同的业务需求也要求修改同一个类。如果两个不同的业务需求会触发同一个类被修改,那么这个类就有两个变化原因,违反了 SRP。Martin 的后续版本进一步用"actor 视角"来定义,即一个类应该只服务于一个参与者(actor)。

SRP 的本质是"变化对齐":把会因同一原因而变化的代码放在一起,把会因不同原因而变化的代码分开。它管的是"代码的耦合方向"而非"类的大小"。实践中,一个类可能有很多方法,但只要所有这些方法都服务于同一个变化原因,它就是符合 SRP 的;反之,一个类即使只有两个方法,如果它们服务于两个不同的变化原因,也违反了 SRP。

#
★★★

2. Actor(参与者)视角的 SRP 中每个类服务于一个 actor;多 actor 引起的"胖类"识别

从"参与者(actor)"的角度解释 SRP,并说明如何用它识别"胖类"?

  • Actor 视角下 SRP 的定义(每个类服务于一个 actor)
  • 多个 actor 共享一个类导致的"胖类"问题
  • 用 actor 冲突识别类的拆分时机

Actor 视角是 Martin 对 SRP 的经典阐述:一个类应该只服务于一个参与者(actor),这里的 actor 可以是真实的用户角色、系统、或外部接口的调用方。当多个 actor 共享同一个类时,一个 actor 提出的需求变更会牵连到另一个 actor 所使用的代码,形成"胖类"。例如一个 Employee 类同时被"薪酬系统"和"人事系统"两个 actor 使用,薪酬系统要改薪资计算逻辑,人事系统要改考勤逻辑,两个 actor 的变更都会落在这个类上,导致类被频繁修改且各自的测试互相影响。识别方法:列出这个类的所有方法,问每个方法"谁因为什么业务原因会要求我修改它"。

Actor 视角让 SRP 变得可操作——它不再抽象地谈"职责",而是具体到"谁会因什么需求而要求这个类变化"。当发现同一个类服务于多个 actor 时,就应该按 actor 拆分,让每个类只对单一 actor 负责。这比"按方法数量"判断更可靠。

// 违反 SRP:Employee 同时服务薪酬(CP)与人事(HR)两个 actor
class Employee {
    double calculatePay() { /* CP 逻辑 */ }
    String reportHours() { /* HR 逻辑 */ }
    void save() { /* 持久化 */ }
}
// 按 actor 拆分
class PayCalculator { double calculatePay(Employee e) { ... } }
class HourReporter   { String reportHours(Employee e) { ... } }
#
★★★

3. SRP 与 SoC(Separation of Concerns)的边界中 SoC 是原则、SRP 是具体规则

说明单一职责原则(SRP)与关注点分离(Separation of Concerns, SoC)的关系与边界?

  • 理解 SoC 是更宽泛的通用原则、SRP 是其具体化规则
  • 区分两者在抽象层级上的不同
  • 知道 SoC 涵盖 SRP 之外的多种关注点分离方式

SoC(关注点分离)是一个更宏观的软件工程原则,指"把不同的问题领域分开处理",例如表现层与业务层分离、业务逻辑与持久化分离、日志与核心逻辑分离等。SRP 是 SoC 在"类/模块内部职责划分"层面上的具体规则。可以说 SoC 是"道",SRP 是"术":SoC 回答"为什么要把代码分开",SRP 回答"在类的粒度上具体怎么分"。SoC 的表现形式还包括分层架构、AOP 横切关注点、模块化、微服务拆分等,而 SRP 只聚焦于单一类或模块的变化原因。

理解这个边界有助于避免把两者混为一谈。SRP 是 SoC 在代码单元(类)层面的落地,它强调"变化原因";而 SoC 还关心架构层次、关注点横切等更大的分离维度。面试时应能说出:SoC 是原则,SRP 是规则,SRP 是 SoC 在类粒度上的体现。

#
★★★

4. 业务用例(Use Case)作为模块划分依据;DDD 的聚合(Aggregate)作为变化边界

如何用业务用例(Use Case)作为模块划分依据?DDD 中的聚合(Aggregate)如何作为变化边界?

  • 用 Use Case 指导模块/类划分
  • DDD 聚合的作用与划分原则
  • 聚合作为事务边界与不变式边界

业务用例(Use Case)描述一个用户在特定场景下完成的目标,是划分模块的天然依据:一个用例对应一个用例类(如 RegisterUserUseCase、PlaceOrderUseCase),这样每个用例的变化只影响自己的实现,符合 SRP。DDD 的聚合(Aggregate)是一组以"根实体"为入口、通过业务不变量(invariant)绑定在一起的实体和值对象的集合。聚合定义了变化边界:外部对象只能通过聚合根访问聚合内部,聚合内部的一致性由聚合根在事务内保证,跨聚合的修改通过最终一致性完成。聚合即"变化边界"——凡是会改变同一组不变式的代码都应在同一个聚合内,否则应拆成不同聚合。

这一问把 SRP 从"类"提升到"用例/聚合"粒度。Use Case 是业务层面的模块划分,聚合是领域建模层面的边界。两者都遵循"把因同一原因变化的东西放一起"的思想,是 SRP 在更大粒度的体现。

#
★★★

5. 变化驱动的模块边界 vs 复用驱动的模块边界(Utility、Helper)的冲突

"变化驱动的模块边界"与"复用驱动的模块边界"(如 Utility、Helper 类)有何冲突?如何取舍?

  • 变化驱动 vs 复用驱动两种划分思路
  • Utility/Helper 类的"复用"诱惑与 SRP 冲突
  • 如何权衡

变化驱动是按"是否会因同一原因而变化"来划分模块,符合 SRP;复用驱动是按"哪里用到这段代码"来划分,典型代表是 Utility、Helper 类。两者常常冲突:一个方法被多个业务模块复用,于是被抽到公共 Utility 类,但这个方法可能会因为多个调用方的不同需求而变化,导致 Utility 类成为"变化原因"的集合,违反 SRP。折中方案:Utility 类只放"稳定、无业务含义"的纯工具函数(与业务无关的字符串、日期、集合操作),这类函数几乎不会因业务变化而变;而一旦某个函数带有业务含义并被多个业务变化驱动,就应当用依赖注入或策略把它放回业务侧,而不是堆进 Utility。

关键判断是"这段代码会不会因为不同的业务原因而变化"。纯工具函数(无业务语义)放 Utility 是合理的;含业务语义的复用代码应通过抽象、策略或领域服务来共享,避免"一个 Helper 类承载多个变化原因"。过度抽取 Utility 往往导致"垃圾场"类,本质上是对 SRP 的违反。

#
★★★

6. SRP 的常见误解澄清中「一个类只有一个公共方法」不是 SRP,如何用真实重构案例向团队演示「按变化原因拆分」与「按方法数量拆分」的差异?

澄清"一个类只有一个公共方法"为何不是 SRP,并用真实重构案例说明"按变化原因拆分"与"按方法数量拆分"的差异?

  • 破除"SRP 等于类小/方法少"的误解
  • 用具体案例演示两种拆分思路的不同结果
  • 理解 SRP 的本质是变化原因而非粒度

"一个类只有一个公共方法"是常见的对 SRP 的误解。SRP 关注的是"变化原因",不是"方法数量"。一个类可以有多个公共方法,只要它们都因同一个原因变化,就符合 SRP;反之,一个类即使只有一个方法,但如果它同时被多个 actor 的业务变化驱动,也违反 SRP。举一个真实案例:一个 Report 类有 generateText() 和 generatePdf() 两个方法,按"方法数量"拆分会把它拆成 TextReport 和 PdfReport 两个类,但这两种输出格式可能都因"报表内容字段变化"这一相同原因而变,拆分后反而让内容变更要改两个类,违反 OCP 且更碎。而按"变化原因"拆分:如果"内容字段"和"输出格式"是两个独立变化原因,才应把"内容生成"与"格式渲染"拆开。所以向团队演示时,应强调"先问为什么变,再决定拆不拆"。

这个案例说明拆分的依据是"变化原因的边界"而非"方法个数的多寡"。按方法数量拆分是机械的、可能适得其反;按变化原因拆分才能让每个类只因一个原因而变。好的拆分应该让"类的外部变化"与"类的内部修改"一一对应。

#
★★

7. 微服务的"职责"边界中 Bounded Context(限界上下文)作为 SRP 的服务级体现

在微服务架构中,限界上下文(Bounded Context)如何作为 SRP 在服务级上的体现?

  • 理解 Bounded Context 与 SRP 的类比关系
  • 一个微服务对应一个限界上下文
  • 防止服务内多上下文混杂

Bounded Context(限界上下文)是 DDD 中"一个模型在特定上下文中有明确边界"的概念。每个限界上下文有自己独立的领域模型、术语和通用语言,微服务通常以一个限界上下文为边界来划分。这正是 SRP 在服务粒度上的体现:每个微服务只服务于一个业务上下文(一个变化原因),不同上下文(如"订单"与"库存")的模型和变化原因不同,应拆成独立服务,避免一个服务承载多个上下文导致"胖服务"、部署耦合、团队冲突。跨上下文通过明确的上下文映射(Context Map)和防腐层(Anti-Corruption Layer)协作。

把 SRP 从"类"放大到"服务":一个微服务只应有一个变化原因,对应一个限界上下文。服务内部若混入多个上下文,就会因多个业务需求而常变常部署,违背 SRP 和高内聚原则。面试时点出"限界上下文 = 服务级 SRP"即可。

#
★★

8. "上帝类"(God Class)的诊断中 field count、method count、fan-in、fan-out

如何通过 field count、method count、fan-in、fan-out 等指标诊断"上帝类"(God Class)?

  • 上帝类的定义与危害
  • 诊断指标及各指标含义
  • 使用静态分析工具进行检测

上帝类(God Class)是指一个类承担了过多职责、控制了大量其他对象、包含了超多字段和方法的类,是 SRP 最严重的违反。可用指标诊断:field count(字段数,过多说明类状态混乱)、method count(方法数,过多说明职责过多)、fan-in(被多少类引用,过高说明它成为中心枢纽)、fan-out(引用了多少其他类,过高说明它依赖过多、编排一切)。理想上,一个类的 field count 和 method count 应维持在合理范围,fan-in 和 fan-out 应均衡。工具如 PMD 的 GodClass、SonarQube 的复杂度规则、Java 的 CK metrics(WMC、DIT、NOC、CBO)可自动检测。

上帝类对应"高 CBO(耦合度)+ 高 WMC(方法复杂度)"的组合。诊断的核心是"职责是否单一":一个类同时是数据容器、业务逻辑、IO 编排、状态管理,就是上帝类。用这些量化指标可以从代码体检中快速定位,再按变化原因拆分。

#
★★

9. "业务-技术"两层职责的拆分中业务代码与基础设施代码的隔离(Hexagonal Architecture)

如何用六边形架构(Hexagonal Architecture)实现"业务代码"与"基础设施代码"的隔离?

  • 六边形架构的核心思想
  • 端口(Port)与适配器(Adapter)的划分
  • 依赖方向由外向内

六边形架构(Hexagonal/Ports and Adapters)把系统分为"核心业务"与"外围基础设施"两部分。核心业务(domain)位于内层,不依赖任何技术细节;外围通过"端口(Port)"定义接口,由"适配器(Adapter)"实现这些端口来对接数据库、HTTP、消息队列等外部资源。依赖方向始终指向核心:核心定义端口,适配器实现端口,基础设施依赖核心而非反向。这样业务代码不再因数据库切换、框架升级而改变,符合 SRP 中"业务变化原因是业务、技术变化原因是技术"的分离。

这是 SRP 在"业务-技术"两层上的体现:业务逻辑不应与技术栈耦合。核心通过端口抽象与外部交互,替换数据库或 Web 框架时只改适配器,不改核心。这也与 DIP(依赖倒置)相呼应——核心定义抽象,适配器依赖抽象。

#
★★

10. SRP 的识别信号中一个类有多个变化原因?

如何识别一个类是否有多个变化原因(违反 SRP)?

  • 识别"变化原因"的具体方法
  • 常见信号:if-else 按业务类型分支、多个主语、多个"为什么改"的回答
  • 用"为什么这个类会被修改"引导分析

识别一个类是否有多个变化原因,核心方法是问"这个类会因为什么而被修改",并列出所有触发修改的理由。常见信号包括:类中有多个明显不同的 if-else 分支(按业务类型、角色、格式等切换);类的方法明显服务于不同主语/领域(如一个类既管订单又管支付);类同时处理字段校验、业务逻辑与持久化;类同时被多个互不相关的模块调用且各自对它有不同修改需求。如果回答"为什么改这个类"时能列出两个以上不相关的理由,就说明存在多个变化原因。

识别信号的本质是"变化原因的划分"。一个类只要对"哪些不同需求会触发它修改"给出多个互不相关的答案,就违反了 SRP。判断时借助"actor 视角"和"修改频率"也很有帮助:统计不同模块的修改是否都落在同一类上,即可识别类的主要变化来源;更进一步,若不同模块的修改都落在这个类上,说明它承载了多个变化原因。

#
★★

11. SRP 与横切关注点的边界中日志、鉴权、事务等横切逻辑应归入 AOP/装饰器还是业务类,如何避免职责蔓延又不破坏可读性?

日志、鉴权、事务等横切关注点应放入 AOP/装饰器还是业务类?如何避免职责蔓延又不破坏可读性?

  • 横切关注点(cross-cutting concern)的概念
  • AOP/装饰器 vs 业务类内的取舍
  • 可读性与职责蔓延的平衡

日志、鉴权、事务、监控等横切关注点不属于业务类的核心职责,不应硬塞进业务类,否则会造成职责蔓延(每个业务方法重复样板代码)。首选方案是用 AOP(切面)或装饰器把横切逻辑集中到业务类之外,业务类只保留核心业务逻辑。但过度 AOP 会降低可读性、让人难以追踪调用链。平衡做法:对"无业务语义、声明式即可"的横切(事务、日志、限流)用注解 + AOP 实现;对"有业务语义、需显式参与"的逻辑(如权限判定影响业务分支)应显式调用或保留在业务层,避免魔法般地隐藏关键行为。

横切关注点是 SoC 的典型应用场景。AOP/装饰器把"与业务无关"的重复样板抽离,业务类保持单一职责;但若把"有业务语义"的判断也藏进切面,会破坏可读性与可测试性。判断标准是"这段横切逻辑是否对业务结果有决定性影响、是否需显式可见"。

#

12. "核心-支撑-通用"三层架构(Clean Architecture、DDD 分层)的职责

解释 Clean Architecture 与 DDD 分层中"核心-支撑-通用"三层的职责划分?

  • Clean Architecture 的依赖规则
  • 核心(领域)、支撑(应用/基础设施)、通用(共享)的职责
  • 依赖方向由外向内

Clean Architecture 与 DDD 分层都强调"核心-支撑-通用"的职责划分。核心(Core/Domain)是领域模型与业务规则,是系统的心脏,不依赖任何框架;支撑(Application/Infrastructure)包括应用服务、适配器、持久化、UI 等,围绕核心提供技术能力;通用(Shared/Common)是跨层共享的、无业务语义的工具与基础类型。依赖规则是"由外向内":外层依赖内层,核心不依赖外层。这样核心的变化原因(业务需求)与支撑的变化原因(技术选型)被隔离,符合 SRP。

三层划分本质是"按变化原因分层":核心因业务变、支撑因技术变、通用因共享需求变。核心最稳定,支撑最易变。依赖方向指向核心,保证核心不被技术细节污染。

#

13. SRP 与类拆分中按职责 vs 按功能?

类的拆分应按"职责"还是按"功能"?两者有何区别?

  • 职责与功能的概念区别
  • 按职责拆分更符合 SRP
  • 避免简单的"按功能"机械拆分

按"职责"拆分比按"功能"拆分更符合 SRP。职责(responsibility)是"一个类为什么而变"——它对应一个变化原因;功能(function)是"一个类能做什么"——它只是一组操作。按功能拆分可能把关联的代码拆散(如把所有"保存"方法聚成 SaveService),而按职责拆分则把"因同一原因变化"的代码聚在一起。例如一个类同时有"校验订单"和"计算折扣"两个功能,但两者都因"订单规则变化"而变,那么它们属于同一职责,不应拆开;只有当"校验"和"折扣"由不同业务原因驱动时才应拆分。

关键在"变化原因"而非"能做什么"。按功能拆分是表面的、机械的,容易产生"按操作聚合"的伪服务;按职责拆分是面向变化的,能真正实现 SRP。

#

14. SRP 的过度拆分中类爆炸的代价?

过度应用 SRP 导致"类爆炸"的代价是什么?

  • 过度拆分的表现与代价
  • 类爆炸带来的导航、内存、测试成本
  • 平衡粒度

过度应用 SRP 会导致"类爆炸"(class explosion):一个逻辑被拆成几十个碎片类,每个类只有一两个方法,带来多重代价:代码库导航困难(找逻辑要跨多个文件)、上下文跳跃增加认知负担、依赖关系复杂化、实例数量增多带来轻微内存与构造开销、测试需要在更多单元间协调。更严重的是,过度拆分可能把"因同一原因变化"的代码拆散,反而破坏了内聚,改一个需求要改多个类。

SRP 是原则而非教条,粒度要结合内聚与可读性权衡。拆分的理想状态是"类小而内聚、变化半径最小",而不是"类越小越好"。当拆分反而增加跨类协调成本时,就是过度设计。

#

15. SRP 在模块/函数层面的应用?

SRP 如何应用在模块和函数层面?

  • SRP 不限于类,函数与模块同样适用
  • 单一职责函数(做一件事)的价值
  • 模块级 SRP 与包结构

SRP 不仅适用于类,也适用于函数和模块。函数层面:一个函数应该只做一件事,只有一个变化原因,函数名应能准确描述其职责——这常与"函数提炼"(Extract Function)结合,长函数拆成多个小函数,每个小函数只处理一个逻辑子问题。模块/包层面:一个模块(包)应承载一个内聚的职责领域,包内成员因同一原因变化,包与包之间通过稳定接口解耦。函数级 SRP 提升可读性、可测试性,模块级 SRP 提升架构清晰度。

把 SRP 下沉到函数和模块,是 Clean Code 倡导的实践。小函数容易命名、测试、复用。模块级 SRP 决定包结构是否清晰。三者(类、函数、模块)共同构成"变化原因"在不同粒度的对齐。

#

16. SRP 与测试中单一职责的类更易测试?

为什么单一职责的类更易于测试?

  • 单一职责类测试场景少、依赖少
  • 测试隔离与原因追溯
  • 与依赖注入的配合

单一职责的类更易测试,原因在于:职责单一意味着测试场景少——一个类只有一种行为/一个变化原因,测试用例覆盖其行为更直接,不需要为多个互不相关的职责构造测试;依赖更少——职责单一的类通常依赖更少,便于注入 mock 或 stub,测试更轻量;失败定位更准——测试失败时能快速定位到具体职责,而不是在一个"什么都能做"的类里排查;Mock 边界清晰——单一职责类作为依赖时接口简单,mock 容易。

测试友好是 SRP 推导出的重要收益。类职责单一 → 输入输出关系清晰 → 边界条件少 → 测试简单且稳定。这也反过来成为拆分类的动机:当某个类难以测试时,往往是因为它职责过多。

#

17. SRP 的实践案例中拆分后的类职责表述?

给出一个 SRP 拆分案例,并说明拆分后的每个类如何用一句话准确表述其职责?

  • 拆分后的类职责表述方法
  • 用"动词 + 领域 + 目标"描述单一职责
  • 验证拆分是否合理

以 OrderProcessor 类为例,它同时负责校验订单、计算价格、保存数据库、发送邮件——四个职责。按 SRP 拆分为四个类并各自用一句话表述职责:OrderValidator(负责校验订单是否满足业务规则)、PriceCalculator(负责根据优惠等计算订单总价)、OrderRepository(负责订单的持久化读写)、OrderNotifier(负责向用户发送订单通知邮件)。一句话职责表述的格式通常是"谁 + 做什么 + 为什么",如"OrderValidator 负责校验订单,使其符合业务规则"。若一个类无法用一句话准确的职责表述,说明它仍承载了多个职责,需继续拆分。

职责表述是检验拆分是否到位的手段:一个好类能用一个简洁句子说明"它负责什么、为什么存在"。拆分后类的职责表述应互不重叠、共同覆盖原逻辑。若某类职责描述中出现"并且/同时/此外"等连接词,就是职责蔓延的信号。