应用命名的重构转换来改善代码结构,同时不改变其行为。当用户提到“重构这个”、“代码坏味”、“提取方法”、“替换条件”、“技术债务”、“移动方法”、“内联变量”、“分解条件”或“清理这段混乱的代码”时使用。同样适用于清理遗留代码、通过重构为新增功能做准备,或识别适合特定代码坏味的转换。涵盖坏味驱动的重构、安全转换序列和测试保护。关于代码质量基础,请参见 clean-code。关于管理复杂性,请参见 software-design-philosophy。
重构模式框架
一种有纪律的方法,用于改善现有代码的内部结构,而不改变其可观察的行为。每次重构都遵循相同的循环:验证测试通过,应用一个小的结构变化,验证测试仍然通过。
核心原则
重构不是重写。它是一系列小的、行为保持的转换,每个转换都有测试支持。 你从不改变代码的功能——只改变其组织方式。大爆炸式重写之所以失败,是因为它们将结构变化和行为变化混在一起,使得无法知道是什么破坏了功能。
基础: 糟糕的代码是在时间压力下交付的自然结果,而不是性格缺陷。代码坏味是结构退化的客观信号;坏味目录告诉你在哪里查找,重构目录告诉你做什么。
评分
目标:10/10。 根据八个快速诊断行中有多少通过来评分结构质量——score = round(passed / 8 × 10),当单个坏味严重时向下调整。等级:
- 9-10:没有明显的坏味,每个函数只做一件事,名称揭示意图,消除重复,条件语句在适当的地方使用多态,测试覆盖重构后的路径。
- 5-6:存在一些坏味(长方法、一些重复),但结构基本合理。
- ≤3:普遍存在坏味——混乱的条件语句、上帝类、到处重复——或者没有测试可以安全重构。
始终说明当前分数,指出导致分数下降的坏味,并列出达到10/10所需的具体重构。
重构模式框架
系统改善代码结构的六个关注领域:
1. 代码坏味作为触发器
核心概念: 代码坏味是更深层结构问题的表面指标——不是错误,而是设计使代码更难理解、扩展或维护的信号。每个坏味都对应着修复它的命名重构。
为什么有效: 命名的坏味为团队提供了客观标准,而不是主观的“我不喜欢这个”——“这是依恋情结”直接指向修复方法。
关键见解:
- 坏味分为五个家族:臃肿者、面向对象滥用者、变更阻碍者、可有可无者、耦合者
- 长方法是最常见的坏味;重复代码是最昂贵的坏味
- 需要注释来解释做什么的方法是坏味——提取并命名该代码块
- 霰弹式修改(一个更改,多个类)和发散式变化(一个类,多个更改原因)是职责错位的相反信号
- 基本类型偏执——使用原始字符串/整数而不是小型领域对象——传播错误和重复
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 方法超过10行 | 提取方法 | 将循环体提取到 calculateLineTotal() |
| 一个更改触及多个类(霰弹式修改) | 移动方法/字段 | 将分散的行为收集到一个类中 |
| 多个方法中相同的参数 | 引入参数对象 | startDate, endDate → DateRange |
| 复制粘贴的逻辑 | 提取方法 + 上移方法 | 通过公共方法或基类共享 |
当需要命名坏味及其修复时,请参见 references/smell-catalog.md——所有五个家族(臃肿者、OO滥用者、变更阻碍者、可有可无者、耦合者)及其检测启发式和对应的重构。
2. 组合方法
核心概念: 大多数重构从这里开始:将长方法分解成更小的、命名良好的片段,读起来像散文——高层步骤委托给清晰命名的辅助方法。
为什么有效: 短方法加上揭示意图的名称消除了注释,使错误一目了然,并支持重用;当名称说明一切时,方法调用几乎没有阅读成本。
关键见解:
- 提取方法是最重要的单一重构——首先掌握它
- 想写注释?提取代码块并用注释作为方法名
- 当方法体与名称一样清晰时,内联方法——没有价值的间接是噪音
- 用查询替换临时变量用于在多个地方使用的计算值;拆分临时变量当一个临时变量服务于两个目的时
- 用方法对象替换方法当局部变量过于混乱无法提取时——它们变成字段
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 带有注释的代码块 | 提取方法 | // check eligibility → isEligible() |
| 只使用一次的临时变量 | 内联变量 | 删除 const price = order.getPrice() |
| 简单的委托方法 | 内联方法 | 如果只使用一次,内联 return deliveries > 5 |
| 包含许多混乱局部变量的方法 | 用方法对象替换方法 | 局部变量变成新类中的字段 |
当应用任何方法级转换时,请参见 references/composing-methods.md——提取/内联方法、提取/内联变量、用查询替换临时变量、拆分临时变量和用方法对象替换方法的逐步机制和前后代码示例。
3. 在对象之间移动特性
核心概念: 面向对象设计的关键决策是职责放在哪里。当依恋情结、过度耦合或类大小不平衡表明方法或字段在错误的类中时,将其移动到它所属的地方。
为什么有效: 方法远离其使用的数据会创建不可见的跨类依赖,导致一个逻辑更改波及多个文件——霰弹式修改。将方法和数据放在一起将更改限制在一个类中。
关键见解:
- 当方法使用另一个类的特性多于自己的特性时,移动方法;移动字段同理
- 当一个类做两件事时,提取类——沿着变化轴拆分;当一个类做得太少时,内联类
- 隐藏委托强制遵循迪米特法则;移除中间人当转发成为类的全部时撤销它
- 根据具体情况解决这种张力:当委托链不稳定时隐藏委托,当中间人纯粹是转发时移除它
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 方法羡慕另一个类 | 移动方法 | 将 calculateShipping() 从 Order 移动到 ShippingPolicy |
| 上帝类超过500行 | 提取类 | 将 Address 字段/方法提取到自己的类中 |
客户端调用 a.getB().getC() |
隐藏委托 | 添加 a.getCThroughB() |
| 类只转发调用 | 移除中间人 | 让客户端直接调用委托 |
当决定职责归属时,请参见 references/moving-features.md——移动方法/字段、提取/内联类、隐藏委托和移除中间人的机制。
4. 组织数据
核心概念: 原始数据——魔法数字、暴露的字段、整数类型代码——会引发微妙的错误并分散领域知识。用封装行为并强制不变量的对象替换基本类型。
为什么有效: 一个 int 金额没有舍入规则或货币代码;一个 Money 对象封装了所有这些,因此业务规则位于一个地方,类型系统在编译时捕获错误。
关键见解:
- 用符号常量替换魔法数字——最简单的数据重构;它命名了意图
- 用对象替换数据值治愈基本类型偏执(
EmailAddress、Money、Temperature) - 封装字段和封装集合——永远不要暴露原始字段或可变的内部列表
- 当类型代码影响行为时,用子类替换类型代码;当子类化不切实际时,用策略替换
- 当需要同一性语义时,将值对象改为引用对象(一个共享的
Customer,而不是副本)
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
if (status == 2) |
替换魔法数字 | if (status == ORDER_SHIPPED) |
String email 到处传递 |
用对象替换数据值 | 带有验证的 EmailAddress 类 |
| Getter 返回可变列表 | 封装集合 | 返回 Collections.unmodifiableList(items) |
int typeCode 带有 switch |
用子类替换类型代码 | Employee → Engineer, Manager |
当用对象替换基本类型时,请参见 references/organizing-data.md——用对象替换数据值、将值对象改为引用对象、替换魔法数字、封装字段/集合以及替换类型代码变体的机制。
5. 简化条件逻辑
核心概念: 深度嵌套的 if/else 树、长 switch 和分散的空检查是最难阅读且最容易出错的代码。命名的重构分解、合并和用更清晰的结构替换条件语句。
为什么有效: 一个六分支的条件语句迫使读者在脑海中模拟每条路径;命名良好的提取分支是自文档化的,多态消除了整个类别的“忘记这种情况”的错误。
关键见解:
- 分解条件:将条件、then 分支和 else 分支提取为命名方法
- 合并条件表达式:将具有相同结果的条件合并为一个命名检查
- 用卫语句替换嵌套条件:提前处理边缘情况并返回,保持主路径不缩进
- 用多态替换条件是类型条件语句的黄金标准
- 引入特例(空对象)消除了分散的
if (x == null)检查;引入断言使假设快速失败
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
带有复杂条件的长 if |
分解条件 | 提取 isSummer(date) 和 summerCharge() |
深度嵌套的 if/else |
用卫语句替换 | 边缘情况优先,提前返回,扁平主路径 |
| 基于对象类型的 switch | 用多态替换条件 | 每个类型实现自己的 calculatePay() |
到处是 if (customer == null) |
引入特例 | 带有安全默认行为的 NullCustomer |
当理清分支时,请参见 references/simplifying-conditionals.md——分解/合并条件、卫语句、用多态替换条件、特例和断言的前后示例。
6. 安全重构工作流
核心概念: 只有在测试的保护下,重构才是安全的。工作流是机械的:运行测试(绿色),应用一个小的转换,运行测试(绿色),提交。如果测试变红,回滚——不要调试失败的重构。
为什么有效: 小步骤使失败显而易见(是你做的最后一件事),回滚只需几秒钟;调试失败的大爆炸式重写需要几天。
关键见解:
- 三次法则:容忍一次重复,注意两次重复,在第三次出现时重构
- 预备性重构:在添加功能之前重构以使功能易于添加;理解和捡垃圾重构在阅读和接触代码时持续改进代码
- 何时不重构:重写更容易、没有测试且添加测试不可行、或者代码很快会被删除
- 先为清晰重构,然后分析并优化测量到的瓶颈——清晰的代码更容易调优
- 通过抽象分支和并行变更可以在生产环境中进行大型重构,而无需长期存在的分支
代码应用:
| 上下文 | 模式 | 示例 |
|---|---|---|
| 即将添加功能 | 预备性重构 | 先清理插入点 |
| 第三次出现相同逻辑 | 三次法则 | 现在提取共享逻辑 |
| 生产环境中大型 API 变更 | 通过抽象分支 | 添加抽象层,迁移调用者,移除旧路径 |
| 重命名广泛使用的方法 | 并行变更 | 添加新方法,弃用旧方法,迁移,移除 |
在进行大型或有风险的重构之前,请参见 references/refactoring-workflow.md——完整的绿色到绿色周期、何时(不)重构、性能、通过抽象分支和并行变更。
常见错误
| 错误 | 为什么失败 | 修复 |
|---|---|---|
| 没有测试的重构 | 没有安全网来检测行为变化 | 先编写表征测试 |
| 大爆炸式重写 | 混合结构变化和行为变化;无法调试 | 尽可能小的步骤,每一步后测试 |
| 在添加功能的同时重构 | 同时戴两顶帽子——两种变化都无法验证 | 先重构(提交),然后添加功能(提交) |
| 重命名而不更新调用者 | 构建失败或死代码 | 使用 IDE 重命名;搜索所有引用 |
| 提取过多过小的方法 | 当名称不佳时,间接性没有清晰度 | 每个名称必须消除阅读方法体的需要 |
| 忽略坏味目录 | 重新发明修复方法而不是应用经过验证的配方 | 学习命名的坏味;每个都对应重构 |
| 重构注定要废弃的代码 | 在注定要废弃的代码上打磨是浪费 | 检查代码的生命周期是否值得投资 |
| 在重构的同时优化 | 混淆清晰度和性能 | 先清晰,然后分析,然后优化热点路径 |
快速诊断
| 问题 | 如果否 | 行动 |
|---|---|---|
| 开始前测试是否通过? | 没有安全网 | 先编写或修复测试——永远不要在红色状态下重构 |
| 你能说出你正在修复的坏味名称吗? | 凭直觉重构,而不是目录 | 识别坏味,应用其规定的重构 |
| 每个方法是否在约10行以下? | 可能存在长方法 | 提取方法为命名步骤 |
| 每个类是否只有一个更改原因? | 发散式变化或大类 | 提取类以分离职责 |
| 是否存在重复的代码块? | 最昂贵的坏味 | 将共享逻辑提取到公共方法/基类 |
| 条件语句在适当的地方是否使用多态? | 存在 switch 语句 | 用多态替换条件 |
| 你是否在每一步后提交? | 有丢失工作、混合更改的风险 | 在每次绿色到绿色转换后提交 |
| 更改后代码是否更易读? | 重构增加了复杂性 | 回滚并尝试不同的方法 |
进一步阅读
改善现有代码的权威指南:
- 《重构:改善既有代码的设计(第2版)》 by Martin Fowler
- 《有效处理遗留代码》 by Michael Feathers(无测试代码的伴侣)
- 《代码整洁之道》 by Robert C. Martin(补充命名和风格原则)
关于作者
Martin Fowler 是 Thoughtworks 的首席科学家,敏捷宣言的签署人,以及《重构:改善既有代码的设计》(1999年;第2版2018年)的作者,该书将基于目录的命名重构引入主流开发。他的目录支撑着每个主要 IDE 中的自动化重构工具。






