反模式与代码坏味道

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

1. Primitive Obsession(基本类型偏执)中何时应该用值对象替代散落的字符串/数字参数?

说明 Primitive Obsession(基本类型偏执)反模式,以及何时应该用值对象替代散落的字符串/数字参数?

  • 基本类型偏执的定义
  • 值对象的引入时机
  • 类型安全与行为内聚

Primitive Obsession 指用基本类型(String/int/boolean)表达领域概念大量散落在代码中,问题在于:无类型安全(如把"金额"和"数量"都当 int 混用)、重复校验逻辑(到处校验字符串格式)、语义不清晰(天数与秒数难区分)且难以承载行为。当某个基本类型在多个地方被反复校验、转换或承载业务规则时,应引入值对象(Value Object):把数值/字符串 + 其校验与行为封装成一个类,获得类型安全、单一职责与行为内聚。例如用 Money 类封装金额与币种,用 Quantity 类封装数量与单位,用 Email 类封装格式校验。

值对象是"以类型系统表达领域语义"。判断标准是"重复出现的校验与行为"或"类型混淆风险"。引入值对象可提升可读性、减少重复、增加类型安全,但不要过度封装简单场景。

// 值对象:把金额与币种封装,避免 int/float 混用
public final class Money {
    private final BigDecimal amount;
    private final String currency;
    public Money(BigDecimal amount, String currency) {
        if (amount == null || amount.signum() < 0) {
            throw new IllegalArgumentException("amount must be non-negative");
        }
        this.amount = amount;
        this.currency = currency;
    }
    public Money add(Money other) {
        if (!currency.equals(other.currency)) {
            throw new IllegalArgumentException("currency mismatch");
        }
        return new Money(amount.add(other.amount), currency);
    }
    // equals/hashCode based on amount and currency
}
#
★★★

2. 反模式的系统性治理中如何用静态分析(圈复杂度/内聚度)自动识别并纳入 CI 门禁?

说明如何用静态分析(圈复杂度、内聚度等指标)自动识别反模式,并将其纳入 CI 门禁?

  • 圈复杂度指标
  • 内聚度指标
  • CI 门禁与治理

反模式的系统性治理依赖静态分析工具量化指标:圈复杂度(Cyclomatic Complexity)衡量方法/类的分支复杂度,过高预示 Long Method、复杂 if-else 等坏味道;内聚度(如 LCOM 方法内聚缺乏度、TCC)衡量方法共享字段的程度,LCOM 过高预示 God Class 或职责混杂。工具(如 SonarQube、Checkstyle、PMD)可扫描并输出这些指标。治理流程:把指标阈值纳入 CI 门禁(如单方法圈复杂度 > 10 或类 LCOM > 阈值则构建失败/告警),新增代码必须满足阈值,存量代码按严重度渐进重构;同时用评审清单配合,避免"只改指标不改结构"。

静态指标是"显微镜",能自动暴露反模式信号,但指标是启发式、需设合理阈值避免误报。CI 门禁保证"新代码不引入新债",结合渐进重构清理存量债,形成可持续治理。

#
★★★

3. Speculative Generality 中如何识别为将来可能用到而做的过度抽象,删除时的安全步骤

说明 Speculative Generality(猜测性通用)反模式,如何识别为将来可能用到而做的过度抽象,以及删除时的安全步骤?

  • Speculative Generality 的定义
  • 识别过度抽象
  • 删除时的安全步骤

Speculative Generality 指为"将来可能用到"而做的过度抽象——多余的接口、参数、基类、通用方法,当前没有真实使用者。识别信号:某抽象只有一个实现、参数从不被传非默认值、接口/基类没有调用方、方法带有无用参数等。删除时的安全步骤:先用静态分析/调用图确认抽象确实无使用者(或仅测试引用),再逐步删除(先删无用参数与默认分支,再合并多余接口/基类,最后删除死代码),每步保持可编译可测试(用版本控制与测试保障),如需回退可用 git 恢复。若确实有明确未来需求,可保留但加注释说明依据,避免纯猜测。

YAGNI(你不会需要它)原则下,少做无用的抽象。删除要"先验证无引用、再逐步小步删、保持可测试",避免删错真实依赖。判断"真需求"与"瞎猜"是关键。

#
★★★

4. Message Chains 与 Middle Man 中链式调用如何隐藏真实依赖,中间人只做转发时如何用 Hide Delegate 与 Remove Middle Man 重构

说明 Message Chains 与 Middle Man 反模式,链式调用如何隐藏真实依赖,以及中间人只做转发时如何用 Hide Delegate 与 Remove Middle Man 重构?

  • Message Chains 的链式调用
  • Middle Man 的过度转发
  • Hide Delegate 与 Remove Middle Man 重构

Message Chains 指 a.getB().getC().getD() 这类链式调用,问题在于调用方与真实对象(D)的耦合被隐藏——真实依赖关系不明确,且中间任何一环变化都会牵连调用方。Middle Man 指对象大量方法只是转发给被委托对象、自身不做任何事,形成"中间人"冗余。重构方向恰好相反:对 Message Chains,用 Hide Delegate(隐藏委托)在链的中间对象上暴露意图方法,让调用方不需要知道链结构;对只做转发的 Middle Man,用 Remove Middle Man(移除中间人)让调用方直接使用被委托对象,删除无价值转发。二者都是"减少无意义的间接层"。

两个反模式是间接层的两面:链式调用过度暴露细节,中间人过度隐藏细节。重构目标是"让调用方与真实依赖的耦合清晰且得当"。判断标准是中间对象是否提供了有价值的封装。

#
★★

5. Feature Envy(依恋情结)中方法过度访问其他类数据时如何用 Move Method 纠正?

说明 Feature Envy(依恋情结)反模式,以及方法过度访问其他类数据时如何用 Move Method 纠正?

  • Feature Envy 的定义
  • Move Method 重构
  • 数据与行为归属

Feature Envy 指一个方法访问"其他类"的数据多于"自身类"的数据(如某方法主要调用别的对象的 getter 和运算),表明方法放错了归属对象。纠正方法是用 Move Method:把该方法移动到它真正依赖的数据所在的类中,让"数据与操作它的行为"内聚在一起。例如方法 priceOf(Order o) 大量读取 o.getAmount() 与 o.getRate(),应移动到 Order 类作为 order.price()。Move Method 后,调用方改为调用归属类的意图方法,数据封装性增强、违反点减少。

Feature Envy 本质是"行为与数据错位"。改进方向是让方法靠近它操作的数据。Move Method 要同时迁移相关字段与方法(必要时 Move Field),并保持行为不变。

#
★★

6. God Class 的识别与拆分中按职责内聚度拆分,如何避免拆出新的"上帝类"?

说明 God Class(上帝类)的识别与拆分,如何按职责内聚度拆分,以及如何避免拆出新的"上帝类"?

  • God Class 信号
  • 按职责内聚度拆分
  • 避免新上帝类

God Class 指一个类承担过多职责、字段与方法众多、LCOM(内聚度)低、被大量代码依赖。识别信号:类方法/字段数量大、LCOM 高、改动频繁牵涉多方面。拆分时按职责内聚度分组:分析哪些字段与方法属于同一职责(SRP),用 Extract Class 把每组职责拆成独立类,让同类内的字段与方法高度内聚。避免拆出新的上帝类:拆分前先明确每个新类的单一职责边界,避免"大拆成多个中类";拆出的类应各自有清晰职责,配合依赖注入与接口,防止出现新的聚合型"上帝类"。

拆分的本质是"重构职责边界"。关键是先做职责聚类再拆分,而非盲目按大小切分。拆分后要保证职责单一、内聚度高,并用测试保证行为不变。

#
★★

7. 数据泥团(Data Clump)中重复出现的一组字段如何提取为类,保持行为内聚?

说明数据泥团(Data Clump)反模式,重复出现的一组字段如何提取为类并保持行为内聚?

  • Data Clump 的定义
  • 提取字段组为类
  • 行为内聚

Data Clump 指同一组字段(如 startDate、endDate、name 等)在多个类/方法的参数列表中反复成组出现,并被一起处理。处理方式是用 Extract Class/Introduce Parameter Object 把这一组字段提取为类(如 DateRange、Address),把字段与它们相关的行为(校验、运算)封装进类,实现数据与行为内聚。提取后,多处签名从多个参数变为一个对象,重复消失、语义清晰、行为可复用。注意不要过度提取——只有当字段组确实成组出现且一起被操作时才值得提取。

Data Clump 的修复是"把重复的字段组提升为领域对象"。判断标准是"成组出现 + 一起被操作"。提取既减少参数列表,又让行为内聚于对象。

#
★★

8. 坏味道度量指标中圈复杂度、LCOM(方法内聚缺乏度)在自动识别中的作用与局限?

说明圈复杂度、LCOM(方法内聚缺乏度)在自动识别坏味道中的作用与局限?

  • 圈复杂度的作用
  • LCOM 的作用
  • 指标的局限

圈复杂度(Cyclomatic Complexity = 判定分支数 + 1)衡量代码路径复杂度,高值预示 Long Method、复杂 if-else、可测试性差,是识别"复杂方法"的有用指标。LCOM(Lack of Cohesion of Methods,方法内聚缺乏度)衡量方法共享字段的程度,LCOM 高表示方法间共享字段少、内聚差,预示 God Class 或职责混杂。局限:指标是启发式、非绝对——圈复杂度高有时是合理的(映射表、状态机),LCOM 低也可能因合理设计(策略类、工具类);指标无法捕捉语义职责,需人工结合上下文判断;且度量粒度有限,不同工具定义不一,需设合理阈值并配合评审。

指标的作用是"自动暴露可疑点",局限是"不能替代语义判断"。正确用法是"指标初筛 + 人工复核",避免"为达标而改动但未改善结构"。

#
★★

9. 反模式治理的落地顺序中静态扫描 → 评审清单 → 渐进重构 → 门禁的推进路径?

说明反模式治理的落地顺序:静态扫描、评审清单、渐进重构、门禁的推进路径?

  • 治理推进路径
  • 静态扫描与门禁
  • 渐进重构

反模式治理的落地顺序:先静态扫描(用工具自动识别坏味道,建立基线数据),再制定评审清单(把常见反模式固化为代码评审时的检查项,提升人工复核),然后渐进重构(按优先级分批处理存量坏味道,小步重构+测试保障),最后设门禁(把指标阈值纳入 CI,阻止新债进入,保证长期不退化)。整体是"先自动化发现、再人工把关、再迭代清理、最后固化约束"的闭环。

落地顺序体现"先看清现状、再治理、再防复发"。门禁是最后防线,把治理成果固化。推进时避免一上来就大规模重写,而是渐进、可验证、可回滚。

#
★★

10. Data Class 中只有 getter/setter 的类如何用 Move Method 承载行为,何时应封装集合并暴露意图方法

说明 Data Class(只有 getter/setter 的类)如何用 Move Method 承载行为,以及何时应封装集合并暴露意图方法?

  • Data Class 的识别
  • Move Method 承载行为
  • 封装集合并暴露意图方法

Data Class 是只有 getter/setter、没有行为的"贫血模型",数据被外部代码反复读取与操作,导致逻辑分散在调用方。改善方式:把操作该数据的逻辑用 Move Method 移入类中,让类承载行为(如把计算总额、校验逻辑从外部移入类的方法)。对暴露集合字段的 Data Class,应封装集合(返回不可变视图/副本),防止外部绕过封装直接修改,并暴露意图方法(如 addItem、total),让调用方通过意图方法操作而非直接改集合。何时封装:当集合被外部随意修改、或需要维护集合不变量时,应封装并暴露意图方法。

Data Class 的改进是"为数据加上行为"。Move Method 把散落逻辑归位,封装集合防止破坏封装并暴露意图方法提升可读性。避免让类退化为纯数据容器。

#
★★

11. Long Parameter List 中参数过多的成因与 Introduce Parameter Object、Preserve Whole Object 的取舍

说明 Long Parameter List(过长参数列表)的成因,以及 Introduce Parameter Object 与 Preserve Whole Object 的取舍?

  • 过长参数列表的成因
  • Introduce Parameter Object
  • Preserve Whole Object

Long Parameter List 的成因:方法调用需要传递多个相关数据,或方法被拆解后参数在调用间传递、不断累积。修复方式:Introduce Parameter Object 把逻辑上相关的参数组合成一个对象(参数对象),减少参数个数、提升可读性;Preserve Whole Object 则在方法已持有完整对象时,直接传整个对象而非其多个字段,让方法内部按需取用。取舍:参数对象适合"参数是数据集合、经常一起出现";Preserve Whole Object 适合"调用方已有完整对象、方法需要其中多个字段",但要注意不要传无关字段造成耦合。若参数彼此独立无关联,则不宜强行合并。

过长参数列表的根源常在"方法职责过宽或数据传递无组织"。选参数对象还是整体对象,取决于"参数是否成组相关"与"调用方是否已有完整对象"。修复时保持行为不变。

#

12. Refused Bequest 的识别与处理中子类只使用父类部分行为时,为什么用'以委托取代继承'(Replace Inheritance with Delegation)重构?

说明 Refused Bequest 反模式的识别,以及子类只使用父类部分行为时为什么用"以委托取代继承"重构?

  • Refused Bequest 的定义
  • 以委托取代继承
  • 继承与委托取舍

Refused Bequest(拒绝馈赠)指子类只使用父类的一部分行为,其余被拒绝(可能抛异常或空实现),说明继承关系不成立——子类并不"是"父类的完整特化。处理方式:用"以委托取代继承"(Replace Inheritance with Delegation),让子类持有一个父类对象,通过委托复用需要的部分,而不是通过继承暴露全部。这样避免继承带来的"接口污染"(子类被迫拥有不适用的方法)、打破封装,且更符合组合优于继承。若子类确实需要大部分父类行为,则保留继承。

继承应表达"is-a"关系,子类只愿用部分父类行为时说明关系不成立。委托让类保留所需能力而避免不必要的接口暴露,是更灵活、更贴合"组合优于继承"的做法。

#

13. 重复造轮子(Reinventing the Wheel)的代价与边界中什么场景下自研优于复用(学习、深度定制、合规),如何评估长期维护成本?

说明重复造轮子(Reinventing the Wheel)的代价与边界,什么场景下自研优于复用,以及如何评估长期维护成本?

  • 重复造轮子的代价
  • 自研优于复用的场景
  • 长期维护成本评估

重复造轮子的代价:开发与维护成本高、可能引入 bug 与安全问题、与生态不兼容、缺少社区支持。但自研在某些场景优于复用:学习目的(为理解原理而实践)、深度定制(现有库无法满足特殊需求且扩展成本高)、合规要求(现成库不满足安全/合规标准)、核心能力(自研形成差异化竞争壁垒)。评估长期维护成本时,需对比"复用方的维护成本 + 升级成本 + 集成成本"与"自研的开发成本 + 长期维护人力 + 演进成本",并考虑团队能力、社区活跃度、技术风险。若复用方案的维护成本可控且无明显短板,通常优先复用。

"不重复造轮子"是默认,但"边界"是:当现成方案无法满足核心需求或带来不可控成本时,自研合理。判断核心是"总拥有成本(TCO)与风险"而非"能否自研"。

#

14. God Class 与 Long Method 的危害中为什么类/方法过长会降低可测试性与可维护性?

说明 God Class 与 Long Method 的危害,为什么类/方法过长会降低可测试性与可维护性?

  • God Class 与 Long Method 的危害
  • 可测试性降低的原因
  • 可维护性降低的原因

God Class 与 Long Method 过长,会导致:可读性差(职责混杂、难以理解)、可测试性差(依赖众多、难以构造测试环境、难以覆盖单一路径)、可维护性差(改动一处牵连多处、圈复杂度高、回归风险大)、复用难(过长逻辑无法复用)。长方法/类把多个职责揉在一起,测试时难以隔离单一行为,需要复杂的初始化与 mock;改动时因耦合密集,容易引入回归。此外,长方法难以命名,暗示其承担了多个职责,违背单一职责原则。

可测试性与可维护性的核心是"职责单一、依赖清晰、逻辑可重用"。类/方法过长破坏这些,导致测试困难与维护成本上升。重构(Extract Method/Class)是改善手段。

#

15. 复制粘贴代码与 DRY 的边界中复制后微调有时比抽象更安全,如何用规则判定该不该抽象?

说明复制粘贴代码与 DRY(Don't Repeat Yourself)的边界,为什么复制后微调有时比抽象更安全,以及如何用规则判定该不该抽象?

  • DRY 与复制粘贴的权衡
  • 复制后微调更安全的原因
  • 抽象判定规则

DRY 强调避免重复,但"复制后微调"有时比过早抽象更安全:当两段代码只是"看起来相似"但语义可能分叉(未来会朝不同方向演化)时,强行抽象会引入参数化开关、耦合,反而增加复杂度与风险;过早抽象(Speculative Generality)会让代码难以理解。判定该不该抽象的规则:看"重复是否因同一原因变化"(共同演化)——若两处代码因同一需求变化而变,则抽象;若会因不同原因各自演化,则复制保留独立性。还可参考"三次法则":同一逻辑出现三次以上才倾向抽象,避免为一次重复做过早设计。

DRY 与重复的边界是"变化的耦合"。抽象的前提是"重复会一起变化",否则复制换取独立演化更安全。用"共同演化 + 三次法则"判断,避免抽象与复制两端的过度。

#

16. Shotgun Surgery(霰弹式修改)的识别中一处需求改动散落多处文件,如何用重构收敛?

说明 Shotgun Surgery(霰弹式修改)的识别,一处需求改动散落多处文件时如何用重构收敛?

  • Shotgun Surgery 的定义
  • 识别信号
  • 用重构收敛

Shotgun Surgery(霰弹式修改)指一个需求改动需要修改多个分散的文件/类,说明相关逻辑被拆散,违背内聚。识别信号:为加一个功能要改多处、改动列表常是一组固定文件、相关逻辑分散在不同类。收敛方式:把散落的逻辑用 Move Method / Move Field 归并到同一类(或新建类承载),形成内聚模块;用 Extract Class 把相关职责聚拢;必要时用"分解条件/合并散落判断"把对同一概念的处理集中。重构后,一处需求改动落到一处,减少遗漏与回归风险。

Shotgun Surgery 与内聚相反,是"职责被分散"。收敛目标是让"同一变化点集中在一处"。通过搬移行为与聚合职责,让改动局部化、求安全。

#

17. Shotgun Surgery 与 Divergent Change 中两者分别对应合并 vs 拆分的重构方向?

说明 Shotgun Surgery 与 Divergent Change 的区别,以及两者分别对应合并 vs 拆分的重构方向?

  • 两种坏味道的区别
  • 合并 vs 拆分的重构方向
  • 与单一职责原则的关系:让【变化点】与【修改点】一一对应、内聚清晰

Shotgun Surgery(霰弹式修改)与 Divergent Change(发散式变化)是对偶的坏味道:Shotgun Surgery 是"一个变化要改多个地方"(逻辑被拆散,需合并收敛);Divergent Change 是"一个类因不同原因被修改"(类承担多种职责,需拆分)。重构方向相反:Shotgun Surgery 用 Move Method/Extract 把散落逻辑合并到一处(合并);Divergent Change 用 Extract Class 按单一职责拆分(拆分)。前者解决"改动分散",后者解决"职责混杂",都指向单一职责原则的两个方向。

面试常考这对"对偶"概念。判断标准:改多少处 vs 因多少原因改。方向相反但目标一致——让"变化点"与"修改点"一一对应、内聚清晰。

#

18. 过早优化(premature optimization)与性能敏感代码的取舍中为什么先测量再优化能避免过度设计?

说明过早优化(premature optimization)与性能敏感代码的取舍,为什么先测量再优化能避免过度设计?

  • 过早优化的危害
  • 先测量再优化
  • 性能敏感代码的取舍

过早优化(premature optimization)指在未确认真实瓶颈前就优化代码,导致过度设计、复杂度上升、可读性下降、维护成本高,而收益可能并不在关键路径。正确做法是"先测量再优化":先用性能剖析(profiler)、基准测试定位真实瓶颈,确认热点后再针对性地优化,避免把资源浪费在非关键路径上。但性能敏感代码(如高频调用、低延迟、实时系统)应在设计阶段就考虑性能约束,因为事后重构代价大。取舍原则:先保证正确性与清晰,测量确认瓶颈后再优化,且优化要可测量、可回退。

"先测量再优化"避免主观臆测与过度设计。性能敏感场景需提前设计约束,但具体优化仍以测量为准。目标是"在正确性与清晰的前提下,把优化投入在真实瓶颈上"。

#

19. 以注释代替代码(Commented-Out Code)的坏味道,为什么删除注释掉的代码并用版本控制保留历史更优?

说明以注释代替代码(Commented-Out Code)的坏味道,为什么删除注释掉的代码并用版本控制保留历史更优?

  • Commented-Out Code 的坏味道
  • 版本控制保留历史
  • 删除的理由

Commented-Out Code(注释掉的代码)是坏味道:注释掉的代码会误导读者(让人误以为仍在生效或是有意保留)、增加噪音、且因无人维护而逐渐失步(与当前逻辑矛盾)。更优做法是删除注释掉的代码,因为版本控制(git)已完整保留历史,需要时可随时从历史恢复,无需以注释形式保留。删除的好处:代码更干净、无歧义、无失步风险,且读者不会被"可能是死代码"迷惑。若确需保留某段逻辑,应写成文档说明或重建为可用的代码,而不是以注释存在。

版本控制是"删除的安全网"。注释掉的代码既无价值又增加误导,删掉并用 git 历史兜底是标准做法。这体现了"注释应解释为什么,而非保存旧代码"的原则。

#

20. Lazy Class 中几乎不做事的类如何识别,何时合并到相邻类或直接删除

说明 Lazy Class 的识别,几乎不做事的类如何识别,以及何时合并到相邻类或直接删除?

  • Lazy Class 的识别
  • 合并到相邻类
  • 直接删除

Lazy Class(懒散类)指几乎不做事的类——方法很少、字段很少、没有实际职责,常是重构遗留或计划未落实的产物。识别信号:类方法/字段数量极少、没有调用方或调用方很少、方法体几乎为空。处理方式:若类有少量相关内容,可合并到相邻类(Collapse Class,把其字段与行为并入合理归属的类);若类已无任何职责与调用方,直接删除(用版本控制保留历史)。删除前先确认无引用(静态分析调用图),避免误删真实依赖。

Lazy Class 是过度抽象/重构残留,价值低、增加认知负担。处理是"合并或删除",判断标准是"是否有不可替代的职责"。保留无价值类会掩盖真实结构。