| name | refactoring-patterns |
| description | 重构模式 - 应用命名的转换来改善代码结构而不改变行为。当用户提到"重构这个"、"代码异味"、"提取方法"、"替换条件"、"技术债务"、"移动方法"、"内联变量"或"分解条件"时使用。在清理遗留代码、通过重组为新功能准备代码或识别应用于特定代码异样的转换时也会触发。涵盖异味驱动重构、安全转换序列和测试保护。关于代码质量基础,参见clean-code。关于管理复杂度,参见software-design-philosophy。 |
| license | MIT |
| metadata | {"author":"wondelai","version":"1.1.0"} |
重构模式框架
一种在不改变可观察行为的前提下改进现有代码内部结构的系统化方法。在审查代码、减少技术债务或为新功能准备代码时应用这些命名的转换。每次重构都遵循相同的循环:验证测试通过,应用一个小的结构变更,验证测试仍然通过。
核心原则
重构不是重写。它是一系列保持行为的小的结构转换,每次都有测试支持。 你永远不会改变代码做什么——你改变的是代码的组织方式。采取经过验证的小步骤的纪律是重构安全的保障。大爆炸式的重写会失败,因为它们将结构变更和行为变更结合在一起,使得无法判断是哪个出了问题。
基础: 糟糕的代码不是性格缺陷——它是在时间压力下交付功能的自然结果。代码异味是结构退化的客观信号。命名的重构是修复每种异味的经过验证的机械配方。异味目录告诉你在哪里看;重构目录告诉你做什么。
评分
目标:10/10。 在审查或重构代码时,根据以下原则的遵守情况对结构质量进行 0-10 评分。10/10 意味着:没有明显的异味,每个函数只做一件事,名称揭示意图,消除重复,测试套件覆盖重构后的路径。始终提供当前分数和达到 10/10 所需的具体重构。
重构模式框架
系统化改进代码结构的六个关注领域:
1. 代码异味作为触发器
核心概念: 代码异味是更深层结构问题的表面指标。它们不是 bug——代码可以工作——但它们表明设计使代码更难理解、扩展或维护。每种异味都对应一个或多个修复它的命名重构。
为什么有效: 如果没有共享的异味词汇,代码审查会沦为"我不喜欢这个"的主观判断。命名的异味给团队提供客观标准:"这是特性嫉妒——这个方法使用了另一个类的六个字段,而自己的类只用了一个。"名称直接指向修复方法。
关键见解:
- 异味分为五大类:膨胀类、面向对象滥用、变更阻止者、可有可无类和耦合类
- 长方法是最常见的异味,也是大多数其他重构的入口
- 重复代码是维护成本的最大驱动力
- 需要注释来解释做什么的方法是一个异味——提取该代码块并命名
- 散弹式修改(一处变更需要多处编辑)和发散式变更(一个类因多种原因变更)是相反的,都表明职责错位
- 基本类型偏执——使用原始字符串、整数或数组而不是小型领域对象——会导致整个代码库的错误和重复
代码应用:
| 上下文 | 模式 | 示例 |
|---|
| 方法 > 10 行 | 提取方法 | 将循环体提取到 calculateLineTotal() |
| 类 > 200 行 | 提取类 | 将运输逻辑移动到 ShippingCalculator |
| 基于类型码的 switch | 用多态替换条件 | 为每种订单类型创建子类 |
| 多个方法使用相同参数 | 引入参数对象 | 将 startDate, endDate 组合为 DateRange |
| 方法使用另一个对象的数据 | 移动方法 | 将 calculateDiscount() 移动到 Customer 类 |
| 复制粘贴的逻辑 | 提取方法 + 上拉方法 | 通过公共方法或基类共享 |
参见:references/smell-catalog.md
2. 组合方法
核心概念: 大多数重构从这里开始。长方法被分解为更小的、命名良好的片段。每个提取的部分应该只做一件事,其名称应该说明那件事是什么。目标是像散文一样可读的方法——一系列高级步骤,每个步骤委托给清晰命名的辅助方法。
为什么有效: 具有揭示意图名称的短方法消除了注释的需要,使 bug 显而易见(每个方法足够小,可以一眼验证),并启用复用。当名称告诉你一切时,方法调用的认知成本接近于零。
关键见解:
- 提取方法是最重要的单一重构——首先掌握它
- 如果你想写注释的冲动,提取代码块并使用注释作为方法名
- 当方法体和名称一样清晰时使用内联方法——没有价值的间接是噪音
- 当临时变量持有在多处使用的计算值时,用查询替换临时变量
- 当一个变量被用于两个不同目的时,拆分临时变量
- 当方法过于纠缠无法提取时(许多局部变量相互引用),用方法对象替换方法
代码应用:
| 上下文 | 模式 | 示例 |
|---|
| 带注释的代码块 | 提取方法 | // 检查资格 变为 isEligible() |
| 仅使用一次的临时变量 | 内联变量 | 如果仅使用一次,移除 const price = order.getPrice() |
| 多处使用的临时变量 | 用查询替换临时变量 | 用方法调用替换 let discount = getDiscount() |
| 因不同原因赋值两次的临时变量 | 拆分临时变量 | 引入 perimeterWidth 和 perimeterHeight |
| 琐碎的委托方法 | 内联方法 | 如果 moreThanFiveDeliveries() 只是 return deliveries > 5 且仅使用一次则内联 |
| 具有许多局部变量的复杂方法 | 用方法对象替换方法 | 将方法移入其自己的类,局部变量变为字段 |
参见:references/composing-methods.md
3. 在对象之间移动特性
核心概念: 面向对象设计中的关键决定是将职责放在哪里。当方法或字段放错了类——表现为特性嫉妒、过度耦合或不平衡的类大小——将它移到它属于的地方。
为什么有效: 良好放置的职责减少耦合并增加内聚。当方法生活在它使用数据的类中时,对该数据的变更只影响一个类。放错的方法创建导致散弹式修改的隐形依赖。
关键见解:
- 当方法使用另一个类的特性多于自己的类时,移动方法
- 当字段被另一个类频繁使用时,移动字段
- 当一个类做两件事时,提取类——沿变更轴分割
- 当类做的事情太少不足以证明其存在时,内联类
- 隐藏委托以强制迪米特法则——客户端不应该导航对象链
- 当一个类只做转发调用时,移除中间人
- 隐藏委托和移除中间人之间的张力逐个案例解决:当链不稳定时隐藏委托;当转发成为类的全部时移除中间人
代码应用:
| 上下文 | 模式 | 示例 |
|---|
| 方法嫉妒另一个类 | 移动方法 | 将 calculateShipping() 从 Order 移动到 ShippingPolicy |
| 字段被另一个类频繁使用 | 移动字段 | 将 discountRate 从 Order 移动到 Customer |
| 500+ 行的上帝类 | 提取类 | 将 Address 字段和方法提取到它们自己的类 |
| 只有一个字段的微小类 | 内联类 | 如果没有行为,将 PhoneNumber 合并回 Contact |
| 客户端调用 a.getB().getC() | 隐藏委托 | 添加 a.getCThroughB() 使客户端不知道 C |
| 类只转发调用 | 移除中间人 | 让客户端直接调用委托 |
参见:references/moving-features.md
4. 组织数据
核心概念: 原始数据——魔数、暴露的字段、表示为整数的类型码、平行数组——会产生微妙的 bug 并分散领域知识。用封装行为并强制不变量的对象替换原始表示。
为什么有效: 表示货币金额的 int 没有舍入规则、货币代码或格式化的概念。Money 对象封装了所有这些。当领域概念被表示为一等对象时,业务规则放在一处,验证自动进行,类型系统在编译时捕获错误。
关键见解:
- 用符号常量替换魔数作为最简单的数据重构——它命名了意图
- 用对象替换数据值(基本类型偏执的解法)——将字符串和数字包装在领域对象中(
EmailAddress、Money、Temperature)
- 封装字段——永远不要暴露原始字段;getter/setter 允许你以后添加验证、日志或计算
- 封装集合——返回不可修改的视图;永远不要让调用者变更你的内部列表
- 当类型码影响行为时,用子类替换类型码;当子类不实用时使用策略
- 当你需要标识语义时,将值改为引用(一个共享的
Customer 对象,而不是副本)
代码应用:
| 上下文 | 模式 | 示例 |
|---|
if (status == 2) | 用符号常量替换魔数 | if (status == ORDER_SHIPPED) |
到处传递 String email | 用对象替换数据值 | 创建带验证的 EmailAddress 类 |
| 公共字段 | 封装字段 | 用 order.getTotal() 替换 order.total |
| getter 返回可变列表 | 封装集合 | 返回 Collections.unmodifiableList(items) |
带 switch 的 int typeCode | 用子类替换类型码 | Employee -> Engineer、Manager、Salesperson |
| 重复的客户记录 | 将值改为引用 | 通过注册表共享一个 Customer 实例 |
参见:references/organizing-data.md
5. 简化条件逻辑
核心概念: 复杂的条件——深层嵌套的 if/else 树、长的 switch 语句、散布各处的空值检查——是最难阅读的代码,也最可能包含 bug。命名的重构分解、合并和替换条件为更清晰的结构。
为什么有效: 具有六个分支和嵌套子条件的条件需要读者在心理上模拟每条路径。将条件分解为命名良好的方法使每个分支自文档化。用多态替换条件消除了整个类别的"忘记处理这种情况"的 bug。
关键见解:
- 分解条件:将条件、then 分支和 else 分支提取为命名方法
- 合并条件表达式:将导致相同结果的多个条件合并为一个命名检查
- 用卫语句替换嵌套条件:提前处理边缘情况并返回,保持主路径不缩进
- 用多态替换条件:基于类型的条件的黄金标准——每种类型知道自己的行为
- 引入特例(空对象):通过提供具有安全默认行为的"无"对象来消除
if (x == null) 检查
- 引入断言:使假设显式化,以便在开发期间快速失败
代码应用:
| 上下文 | 模式 | 示例 |
|---|
具有复杂条件的长 if | 分解条件 | 提取 isSummer(date) 和 summerCharge() |
多个 if 返回相同值 | 合并条件 | 合并为 isDisabled() 提前返回 |
深层嵌套的 if/else | 用卫语句替换 | 首先检查边缘情况,提前返回,展平主路径 |
| 基于对象类型的 switch | 用多态替换条件 | 每种类型实现自己的 calculatePay() |
到处都有 if (customer == null) | 引入特例 | 创建具有默认行为的 NullCustomer |
| 代码中的隐藏假设 | 引入断言 | 方法入口处的 assert quantity > 0 |
参见:references/simplifying-conditionals.md
6. 安全的重构工作流
核心概念: 只有在测试包装下重构才是安全的。工作流是机械的:运行测试(绿色),应用一个小的转换,运行测试(绿色),提交。如果测试变红,回退最后的变更——不要调试失败的重构。
为什么有效: 小步骤使得找到出错的地方变得微不足道(就是你最后做的事)。回退失败的步骤只需几秒钟。调试失败的大爆炸重写需要几天。频繁的提交创建可以返回的保存点。
关键见解:
- 重构循环:测试 -> 重构 -> 测试 -> 提交(重复)
- 三次法则:容忍一次重复,注意两次,第三次出现时重构
- 预备性重构:在添加功能之前重构以使功能易于添加
- 理解性重构:在阅读代码时重构以理解它——让它比你发现时更清晰
- 拾荒式重构:每次接触文件时做小的改进(童子军规则)
- 何时不重构:当从头重写更容易时,当没有测试且首先添加它们不可行时,或当代码即将被删除时
- 重构和性能:首先为清晰度重构,然后分析并优化测量的瓶颈——重构后的代码更容易调整,因为热路径被隔离
- 抽象分支和平行变更使生产系统中的大型重构无需功能分支即可完成
代码应用:
| 上下文 | 模式 | 示例 |
|---|
| 准备添加功能 | 预备性重构 | 提取方法以使新功能的插入点清晰 |
| 阅读不熟悉的代码 | 理解性重构 | 重命名变量并提取方法以理解意图 |
| 工作时看到小问题 | 拾荒式重构 | 在继续之前修复异味(童子军规则) |
| 第三次出现相同逻辑 | 三次法则 | 将共享逻辑提取到公共方法 |
| 生产中的大型 API 变更 | 抽象分支 | 引入抽象层,迁移调用者,移除旧路径 |
| 重命名广泛使用的方法 | 平行变更 | 添加新名称,弃用旧名称,迁移调用者,移除旧名称 |
参见:references/refactoring-workflow.md
常见错误
| 错误 | 为什么失败 | 修复 |
|---|
| 没有测试就重构 | 没有安全网——你无法判断行为是否改变 | 首先编写特征测试,然后重构 |
| 大爆炸重写而不是增量步骤 | 结合结构和行为变更;无法调试 | 采取尽可能小的步骤,每次后运行测试 |
| 重构和添加功能同时进行 | 两顶帽子同时戴——你无法验证任何变更 | 分开帽子:先重构(提交),然后添加功能(提交) |
| 重命名而不更新所有调用者 | 破坏构建或引入死代码 | 使用 IDE 重命名重构;搜索所有引用 |
| 提取太多微小方法 | 当名称不佳时创建没有清晰度的间接 | 每个提取的方法必须有消除阅读正文需要的名称 |
| 忽略异味目录 | 重新发明修复而不是应用经过验证的配方 | 学习命名的异味;每个异味对应特定的重构 |
| 重构将被删除的代码 | 浪费精力——给 condemned 代码抛光 | 先问:这段代码的寿命是否值得投资? |
| 重构期间过早优化 | 将清晰度与性能混淆;优化后的代码通常更难阅读 | 首先为清晰度重构,然后分析,然后仅优化测量的热路径 |
快速诊断
| 问题 | 如果否 | 行动 |
|---|
| 开始前测试是否通过? | 你没有安全网 | 首先编写或修复测试——不要在测试红色时重构 |
| 你能说出你正在修复的异味吗? | 你在凭直觉重构,而不是目录 | 从目录中识别异味,然后应用其规定的重构 |
| 每个方法是否少于 ~10 行? | 可能存在长方法 | 应用提取方法将长方法分解为命名步骤 |
| 每个类是否只有一个变更原因? | 发散式变更或大类异味 | 应用提取类分离职责 |
| 是否有重复的代码块? | 重复代码是最昂贵的异味 | 将共享逻辑提取到公共方法或基类 |
| 条件是否在适当时使用了多态? | 仍然存在 Switch 语句或复杂的 if/else 树 | 应用用多态替换条件 |
| 你是否在每个重构步骤后提交? | 你有丢失工作和混合变更的风险 | 每次绿色到绿色的转换后提交 |
| 你的变更后代码是否更易读? | 重构可能增加了复杂度 | 回退并尝试不同的方法 |
参考文件
进一步阅读
本技能基于改进现有代码的权威指南:
关于作者
Martin Fowler 是 Thoughtworks 的首席科学家,也是软件工程领域最有影响力的声音之一。他是《重构:改进现有代码的设计》(1999 年,第 2 版 2018 年)的作者,该书将命名的、基于目录的重构转换概念引入了主流软件开发。Fowler 还是《企业应用架构模式》、《UML 精髓》以及大量关于软件设计、敏捷方法论和持续交付的影响力文章的作者。他是敏捷宣言的签署者,几十年来一直倡导进化设计——通过纪律严明、渐进式的重构而不是前期大型设计来持续改进代码结构的实践。他的重构目录最初用 Java 编写,已被适配到 virtually 每种编程语言,并内置于每个主要 IDE 的自动重构工具中。