关于 refactor
refactor 是依据 Martin Fowler 重构目录整理代码的技能,面向需要改善代码结构、可读性和可维护性的开发者。它强调保持外部行为不变,以小步修改和测试验证为基础。
主要功能
- 01依据代码异味选择重构方法
- 02提供多类 Fowler 重构技术
- 03要求保持外部行为不变
- 04强调小步修改与测试验证
- 05通过决策卡评估重构依据与成效
适用场景
- 01拆分过长的方法或过大的类
- 02消除重复代码并调整职责归属
- 03在测试保护下改善难以维护的代码
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ smallnest/ goal-workflow --skill refactor安装后在 Claude Code 中输入/refactor确认已加载。
详细介绍
重构 — 专家级代码重构
基于 Martin Fowler 的 (第 2 版)目录进行精准的代码重构。在不改变外部行为的前提下,改善结构、可读性和可维护性。循序渐进地演进,而非彻底变革。
适用场景
在以下情况下启用此技能:
- 代码难以理解或维护
- 函数或类变得过于庞大
- 发现代码异味
- 结构不佳导致难以添加功能
- 用户明确要求重构、清理或改进
- 用户说:重构、清理、改进代码、代码异味、提取方法、重命名、简化
黄金法则
以下五条法则不可妥协。违反任何一条,都会让重构沦为鲁莽的编辑。
1. 保持行为不变
只改变代码的实现方式,绝不改变它的功能。如果之前有测试,重构后必须通过。如果重构引入了行为变化,那就不是重构,而是重写。
2. 小步进行
每次变更都应是能够编译并通过测试的最小转换。如果某一步出错,你就能准确知道是哪个变更导致的。重构是一系列微小且安全的转换,而不是一次大规模重写。
3. 版本控制是你的好帮手
开始前先提交。每次成功完成一步后都提交。这样你就可以随时撤销。基于干净的状态创建分支,这样就能毫无顾虑地放弃重构。
4. 测试至关重要
“没有测试,你做的就不是重构,而只是在编辑。”如果目标代码没有测试,先编写特征化测试。这些测试会记录当前行为,帮助你检测回归。
5. 一次只做一件事
绝不要把重构和功能变更混在一起。绝不要同时重构两个不相关的部分。每次提交都应只包含一个重构操作。
不应重构的情况
| 场景 | 操作 |
|---|---|
| 代码能正常工作,而且不会再改动 | 保持原样 |
| 关键生产路径没有测试 | 先编写特征化测试 |
| 截止时间紧迫 | 记录代码异味,之后再重构 |
| 没有明确的目的或收益 | 不要为了重构而重构 |
| 代码从根本上就有问题 | 这是重写,不是重构 |
重构决策准则
原则用于指导判断,而不是各自独立的重写理由。在选择 Fowler 技术之前,先记录一张决策卡:
| 字段 | 必填答案 |
|---|---|
| 主要代码异味 | 一个根本原因,而不是每项原则各列一条 |
| 证据 | 位置、行为、调用方、变更历史或测量结果 |
| 主要原则 | 最具体且适用的原则 |
| 相关原则 | 仅作解释性标签;不要单独计数 |
| 预期影响 | 可观察到的变更扩散、认知负担、重复知识、耦合、延迟故障或经测量的运行时开销减少 |
| 最小重构 | 能解决根本原因且侵入性最低的 Fowler 技术 |
| 基线 / 成功条件 | 如何验证行为得到保留,以及预期收益已经实现 |
使用以下四个决策视角:
| 视角 | 原则 | 需要回答的问题 |
|---|---|---|
| 职责与依赖关系 | SOLID、SRP、OCP、DIP、关注点分离 | 是否混杂了多个变更原因?新增的真实变体是否反复修改稳定逻辑?高层策略是否依赖具体机制?关注点是否越过边界泄漏? |
| 复用与结构 | DRY、组合 | 重复的是相同知识,还是仅仅相似的语法?与继承相比,组合是否能更好地将真实变化局部化? |
| 简洁性与范围 | KISS、YAGNI | 对于当前问题,提议的结构是否更简单?每个抽象是否都有现存的变体或边界作为依据? |
| 运行时与反馈 | 快速失败、先测量 | 能否在更接近无效状态源头的位置失败,同时不改变错误语义?什么基线可以证明问题及其解决效果? |
SOLID 是一个总称。当证据支持 SRP、OCP 或 DIP 时,应使用对应的具体视角,不要再创建第二个 SOLID 问题。LSP 和 ISP 可以标记为 SOLID/LSP 和 SOLID/ISP。DRY 指共享的知识,而不是所有相似代码。只有当继承造成了真实耦合时,才优先考虑组合,而不是默认使用组合。OCP 和 DIP 绝不能成为引入投机性分层的理由,否则会违背 KISS 或 YAGNI。
本技能中的数值阈值——行数、参数数量、方法数量和嵌套深度——都是取决于上下文的候选指标。采取行动之前,请确认存在职责混杂、认知成本、反复变更、重复知识或经测量的运行时影响。
代码异味目录
基于 Fowler 的分类法。重构前,请确定存在哪种代码异味。
膨胀代码
| 异味 | 描述 | 主要重构 |
|---|---|---|
| 过长方法 | 候选指标:方法超过 10-15 行;需确认存在职责混杂或认知成本 | 提取方法、以查询取代临时变量 |
| 大型类 | 候选指标:字段 / 方法众多;需确认存在独立的变更原因 | 提取类、提取子类 |
| 基本类型偏执 | 使用基本类型而不是小型对象 | 以对象取代数据值、以类取代类型码 |
| 参数列表过长 | 候选指标:参数超过 3-4 个;需确认缺少某个概念或反复出现的数据簇 | 引入参数对象、保留整个对象 |
| 数据簇 | 同一组数据同时出现 | 提取类、引入参数对象 |
面向对象滥用
| 异味 | 描述 | 主要重构 |
|---|---|---|
| Switch 语句 | 针对类型码重复使用 switch / if-else | 以多态取代条件表达式、以子类取代类型码 |
| 临时字段 | 字段仅在某些情况下设置 | 提取类、引入 Null 对象 |
| 被拒绝的遗赠 | 子类不使用继承成员 | 以委托取代继承、下推方法 / 字段 |
| 接口不同的替代类 | 执行相似操作但名称不同的类 | 重命名方法、搬移方法、提取超类 |
变更阻碍因素
| 异味 | 描述 | 主要重构 |
|---|---|---|
| 发散式变化 | 一个类因不同原因而发生变更 | 提炼类 |
| 霰弹式修改 | 一个变更需要在多个类中进行许多小改动 | 搬移方法、搬移字段、内联类 |
| 平行继承体系 | 在一个继承体系中添加子类,就必须在另一个体系中添加子类 | 搬移方法、搬移字段 |
可有可无的元素
| 异味 | 描述 | 主要重构 |
|---|---|---|
| 注释 | 解释代码做什么(而非为什么)的注释 | 提炼方法、重命名变量、引入断言 |
| 重复代码 | 多处存在相同的代码结构 | 提炼方法、上移方法、构成模板方法 |
| 懒惰类 | 做得太少、不足以证明其存在价值的类 | 内联类、折叠继承体系 |
| 数据类 | 只有字段和 getter/setter 的类 | 搬移方法、封装字段、封装集合 |
| 死代码 | 未使用的代码、导入项或注释掉的代码块 | 删除它(Git 历史中有记录) |
| 推测性通用性 | 为“将来某天”而编写、但始终未用上的代码 | 内联类、折叠继承体系、移除参数 |
耦合器
| 异味 | 描述 | 主要重构 |
|---|---|---|
| 依恋情结 | 方法使用其他类的数据多于自身的数据 | 搬移方法、提炼方法 + 搬移方法 |
| 不恰当的亲密关系 | 类过于了解彼此的内部细节 | 搬移方法、搬移字段、用隐藏委托替换委托 |
| 消息链 | a.getB().getC().getD().doSomething() | 隐藏委托、提炼方法 |
| 中间人 | 类将所有工作都委托给另一个类 | 移除中间人、内联方法 |
| 不完整的库类 | 库缺少你需要的方法 | 引入外来方法、引入本地扩展 |
重构技术目录
按类别整理,内容取自 Fowler 的目录。每项技术都包含其机械步骤。
组合方法
提炼方法
将代码片段转换为一个方法,并用能说明其用途的名称命名。
步骤:
- 根据代码片段的作用(而非实现方式)为新方法命名
- 将要提炼的代码复制到新方法中
- 确定局部变量:只读变量变为参数,已修改的变量变为返回值
- 传入参数并处理返回值
- 将原代码片段替换为对新方法的调用
- 测试
之前:
void printOwing() {
printBanner();
// Print details
System.out.println("name: " + _name);
System.out.println("amount: " + getOutstanding());
}
之后:
void printOwing() {
printBanner();
printDetails(getOutstanding());
}
void printDetails(double outstanding) {
System.out.println("name: " + _name);
System.out.println("amount: " + outstanding);
}
内联方法
当方法体和方法名一样清晰时,用方法体替换方法调用。
步骤:
- 确认该方法不是多态方法(没有子类重写它)
- 找出所有调用方
- 将每处调用替换为方法体
- 删除方法定义
- 测试
提炼变量
将表达式(或表达式的一部分)的结果赋给一个自我说明的变量。
之前:
if (platform.toUpperCase().indexOf("MAC") > -1 &&
browser.toUpperCase().indexOf("IE") > -1 &&
wasInitialized() && resize > 0) {
// ...
}
之后:
final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1;
final boolean isIEBrowser = browser.toUpperCase().indexOf("IE") > -1;
final boolean wasResized = resize > 0;
if (isMacOs && isIEBrowser && wasInitialized() && wasResized) {
// ...
}
内联临时变量
当临时变量只使用一次,且表达式清晰时,用表达式替换临时变量。
以查询取代临时变量
将表达式提炼为方法。对于只计算一次并重复使用的临时变量,用方法调用替换。
分解临时变量
对赋值多次的临时变量(循环或累积用变量除外),应按职责拆分为多个独立变量。
移除对参数的赋值
不要对参数赋值,改用局部变量。
以方法对象取代方法
当长方法使用许多局部变量,导致难以提炼方法时,将该方法转换为独立的类,并将局部变量作为字段。
替换算法
用更清晰的算法替换现有算法。
在对象之间搬移特性
移动方法
将方法移到最常使用它的类中。
机制:
- 检查该方法在当前类中使用的所有特性
- 检查是否存在多态(子类/超类方法)
- 在目标类中创建该方法,并根据需要进行调整
- 从源对象引用目标对象
- 将源方法改为委托方法,或将其删除
- 测试
移动字段
将字段移到最常使用它的类中。
提取类
当一个类承担了两类工作时,将它拆分。创建一个新类,并将相关字段和方法移过去。
内联类
当一个类几乎什么也不做时,将它合并到最常使用它的类中。
隐藏委托
在服务端创建方法,隐藏委托链。manager = person.getDepartment().getManager() → manager = person.getManager()。
移除中间人
当一个类承担了过多委托工作时,直接调用委托对象。
引入外部方法
当服务端类需要额外的方法,但你无法修改它时,在客户端创建一个方法,并将服务端实例作为第一个参数。
引入本地扩展
当你需要多个外部方法时,创建一个扩展类(子类或包装器)。
整理数据
字段自封装
通过 getter 和 setter 访问字段,即使是在所属类内部也如此。
用对象替换数据值
当数据项需要额外的数据或行为时,将它转换为对象。
之前:
class Order {
private String customer; // Just a string
}
之后:
class Order {
private Customer customer; // Rich object with name, address, credit rating
}
将值对象改为引用对象
当需要在多个地方共享同一个对象实例时使用。
将引用对象改为值对象
当引用对象较小且不可变,并且你希望采用值语义时使用。
用对象替换数组
当数组中包含异构数据时(String[] row = new String[3] ——姓名、分数、胜场),用对象替换它。
复制被观察数据
领域数据存放在 GUI 控件中,但领域逻辑也需要这些数据。将数据复制到领域对象中,并设置观察者来保持两者同步(观察者模式)。将表示层与领域层分离,使二者能够独立演进。
将单向关联改为双向关联
两个类都需要对方的特性,但只有一方持有引用。添加反向指针,并确保两端的修改器都能保持链接一致。仅在确实需要时才添加引用——双向链接会增加耦合,并带来不一致的风险。
将双向关联改为单向关联
存在双向链接,但其中一方已不再使用另一方。删除不需要的方向。这能降低耦合、简化生命周期管理,并避免因过时的反向指针而让“僵尸”对象继续存活。
用符号常量替换魔法数字
用有名称的常量替换字面数字/字符串。
封装字段
将公有字段设为私有,并提供访问器。
封装集合
不要返回原始集合。返回只读视图,并提供添加/删除方法。
用类替换类型码
用具有明确行为的类替换数字/字符串类型码。
用子类替换类型码
当类型码会影响行为时,使用多态而非条件语句。
用状态/策略替换类型码
与子类方式类似,但当类型可在运行时改变时,使用组合。
用字段替换子类
当子类仅在常量数据上有所不同时,用单个类上的字段替换它们。
简化条件表达式
分解条件式
将条件、then 部分和 else 部分分别提取为独立方法。
之前:
if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
charge = quantity * _winterRate + _winterServiceCharge;
} else {
charge = quantity * _summerRate;
}
之后:
if (isSummer(date)) {
charge = summerCharge(quantity);
} else {
charge = winterCharge(quantity);
}
合并条件表达式
合并多个结果相同的条件表达式。
合并重复的条件片段
将每个分支中都出现的代码移到条件语句之外。
移除控制标记
用 break、continue 或 return 替换控制标记。
将嵌套条件替换为守卫子句
对特殊情况使用提前返回,而不是深层嵌套。
之前(箭头代码):
double getPayAmount() {
double result;
if (_isDead) {
result = deadAmount();
} else {
if (_isSeparated) {
result = separatedAmount();
} else {
if (_isRetired) {
result = retiredAmount();
} else {
result = normalPayAmount();
}
}
}
return result;
}
之后:
double getPayAmount() {
if (_isDead) return deadAmount();
if (_isSeparated) return separatedAmount();
if (_isRetired) return retiredAmount();
return normalPayAmount();
}
将条件替换为多态
当条件语句根据对象类型选择不同的行为时,使用子类。
引入空对象
用提供默认行为的空对象替换空值检查。
引入断言
使用断言明确说明假设。
简化方法调用
重命名方法
方法名应说明该方法的作用。如果想不出合适的名称,该方法可能承担了多项职责。
添加参数 / 移除参数
当方法需要更多信息时添加参数。当方法可以通过其他方式获取信息时移除参数。
将查询与修改分离
方法要么返回值,要么改变状态,不能两者兼有。
参数化方法
多个方法执行相似操作但使用不同值 → 合并为一个带参数的方法。
将参数替换为显式方法
反向操作:当参数实质上用于选择不同的行为时,创建单独的方法。
保留整个对象
传递整个对象,而不是从中提取单个字段。
将参数替换为方法
(refactoring.guru:将参数替换为方法调用) 当参数可以根据对象已有的数据计算得出时,移除该参数,并让方法自行调用查询方法。
引入参数对象
将自然相关的一组参数归并到一个对象中。
移除设置方法
移除字段的 setter,并在构造函数中设置该字段,使其不可变。
隐藏方法
当方法不会在类外部使用时,将其设为 private。
将构造函数替换为工厂方法
当你需要比简单调用构造函数更大的灵活性时使用。
将错误代码替换为异常
抛出异常,而不是返回错误代码。
将异常替换为测试
先检查条件,而不是捕获异常。
处理泛化
上移字段 / 方法 / 构造函数体
将子类中相同的字段、方法或构造函数代码移至超类。
下移方法 / 字段
将超类中的行为移至实际使用它的子类。
提取子类
为仅在某些实例中使用的一部分功能创建子类。
提取超类
为相似类的共享功能创建超类。
提取接口
从类的部分公共方法中创建接口。
折叠继承层次
当超类和子类差异不大时,将它们合并。
形成模板方法
在超类中概括算法,让子类补充具体细节。
用委托替换继承
当子类只使用超类的一部分功能时,改用组合。
用继承替换委托
当委托类需要访问委托对象的全部行为时使用。
重构流程
阶段 1:准备
- 如果尚不存在,编写特征测试。这些测试用于记录当前行为——不必优雅,只需足够全面,能够发现回归问题。
- 为所声称的问题记录基线:记录有代表性的行为、改动范围、相关调用方,或性能工作的基准测试/性能分析/查询次数。没有性能基线时,应将性能想法描述为候选方案,而不是已测得的改进。
- 提交当前状态。从干净的工作树开始。
- 为重构创建分支。将其与功能开发分开。
阶段 2:识别
- 嗅探代码异味。 使用上方的异味目录对问题进行分类,将阈值视为候选信号。
- 理解代码。 彻底阅读代码,梳理相关调用方和依赖项,并找出具体的故障或变更场景。
- 选择一项主要原则。 仅将相关原则作为解释;不要将一个根本原因拆分成多个发现。
- 检查制约因素。 确认 DRY 不会造成虚假抽象,OCP/DIP 并非猜测,SRP 不会产生仅转发调用的代码片段,并且组合确实能解决继承耦合问题。
- 选择合适的重构方式。 从目录中选择最小的技术方案,并定义如何检查基线和行为。
阶段 3:重构(小步进行)
每一步:
- 进行一项小改动。 解决主要异味中一个可验证的部分;不要捆绑无关的原则清理工作。
- 编译。 每次改动后,代码都应能编译。
- 运行测试。 所有测试都必须通过。如果未通过,说明你改变了行为。
- 检查接缝。 新接口、适配器或间接层必须能消除调用方的实际复杂性,或支持现有变化;否则,保留更简单的设计。
- 提交。 创建一条类似
refactor: extract validateEmail method的提交说明。
重复以上步骤,直到异味解决。
阶段 4:验证
- 行为。 所有测试、类型检查、编译和手动冒烟测试均通过。
- 结构。 对照决策卡,比较依赖方向、相关调用方、重复知识或变更范围。
- 快速失败语义。 如果验证提前执行,须保留错误类型/代码、聚合、重试、事务和清理行为。
- 性能。 如果性能是改动依据,请在相同的工作负载和环境下重新运行。报告结果和噪声范围;如果没有基线,只报告未观察到明显退化。
- 差异审查。 检查是否存在非预期改动,并确认已满足成功条件。
阶段 5:清理
- 删除过时注释。 如果重构后某条注释变得显而易见,请删除它。
- 检查死代码。 重构后可能会出现未使用的代码。
- 移除推测性接缝。 删除没有当前实际价值的单实现接口、仅转发调用的模块或未使用的扩展点。
- 最终提交。 总结重构步骤。
重构检查清单
代码质量
- 正在处理一个主要异味/根本原因;相关原则不作为单独工作项计算
- 已记录证据、预期影响、基线和成功条件
- 函数大小适合其上下文(行数是候选信号,而非限制)
- 在有助于明确职责边界时,每个函数只做一件事(SRP)
- 没有重复知识(DRY),且未合并独立变化的代码
- 名称描述做什么,而非如何做
- 没有魔法数字或字符串
- 已移除死代码
结构
- 相关代码归组在一起,关注点不会越过预期边界
- 模块边界清晰
- 依赖沿单一方向流动(无循环);DIP 抽象对应实际边界
- OCP/DIP 改动不会引入违反 KISS/YAGNI 的推测性分层
- 仅在能减少实际继承耦合时优先使用组合
- 没有循环依赖
条件判断
- 使用卫语句替代深层嵌套
- 将复杂条件提取到具名方法中
- 使用多态替代类型切换条件判断
- 在适当情况下使用 Null Object 模式
类型安全(类型化语言)
- 为所有公共 API 定义类型
- 未在没有说明的情况下使用
any - 显式标记可空类型
- 使用类/枚举替代类型代码
测试
- 已测试重构后的代码
- 已覆盖边界情况
- 在适当情况下,无效输入/状态会在其来源附近快速失败(Fail Fast)
- 错误、重试、事务和清理语义保持不变
- 每个步骤完成后,所有测试均通过
- 特征化测试记录了重构前的行为
- 性能声明有可比较的前后测量数据
特定语言指南
Java
- 对不应更改的局部变量,优先使用
final - 对机械性步骤,使用 IDE 自动重构功能(Eclipse/IntelliJ)
- 利用类型系统:枚举、record(Java 14+)、密封类(Java 17+)
JavaScript/TypeScript
- 使用解构来减少参数数量
- 对不可变绑定,优先使用
const而非let - 使用 TypeScript 联合类型代替类型代码
- 空值合并运算符(
??)和可选链(?.)可消除冗余的空值检查
Python
- 使用类型提示在重构期间说明意图
- 使用
dataclasses替换元组/数据类模式 - 使用
@property替换 getter - 使用上下文管理器处理资源清理模式
Go
- 优先使用小型接口:接收接口,返回结构体
- 在能提升清晰度时使用命名返回值
- 表驱动测试很适合配合重构使用
- 使用提前返回避免深层嵌套
Rust
- 使用
Result和Option代替错误代码和 null - 使用模式匹配代替 if-else 链
- 实现
Fromtrait 可简化类型转换 - 派生宏可减少样板代码
常见重构步骤
提取方法步骤
- 创建一个以意图命名的新方法
- 将代码片段复制到新方法中
- 确定局部变量 → 参数/返回值
- 在原位置调用新方法
- 测试
使用多态替换条件语句步骤
- 为每种变体创建子类
- 创建一个返回正确子类的工厂方法
- 将条件语句主体移至相应的子类方法中
- 删除条件语句
提取类步骤
- 确定一组连贯的字段和方法
- 创建一个新类
- 在旧类中创建新类的实例
- 一次移动一个字段和方法
- 更新旧类中的引用
- 每次移动后都进行测试
内联类步骤
- 确定目标类的所有调用方
- 将所有方法/字段移至接收类
- 将所有引用重定向到接收类
- 删除空类
- 测试
安全流程
开始操作之前
1. Characterization tests → capture what the code does now
2. Git commit → save a known-good state
3. Branch → isolate refactoring from other work
每一个步骤
1. One change → one refactoring technique
2. Compile → must compile clean
3. Tests → every test must pass
4. Commit → message: "refactor: <technique> <what>"
测试失败时
1. Undo the last change
2. Understand what broke and why
3. Try a smaller step
4. If the test was wrong and behavior was correct, fix the test FIRST, then retry
完成时
1. Full test suite → all tests pass
2. Manual smoke test → quick sanity check
3. Self-review diff → catch unintended changes
4. Final commit → describe the overall transformation
重构中的设计模式
策略模式
替换用于选择算法的条件语句。异味: 根据类型代码进行分支切换,且每个分支的行为不同。技巧: 使用多态替换条件语句 + 提取方法。
模板方法
将通用算法骨架提取到超类中,让子类填充不同变体。异味: 代码重复,仅有细微差异。技巧: 构建模板方法。
状态模式
将每种状态的行为提取到一个类中,以此替换基于状态的条件语句。异味: 根据状态字段进行分支切换,且行为有所不同。技巧: 使用状态/策略替换类型代码。
组合模式
以统一方式处理单个对象和对象组。异味: 客户端代码需要对单个对象和集合分别进行特殊处理。技巧: 提取接口 + 创建组合对象。
装饰器模式
通过包装对象动态添加行为。异味: 针对可选行为的条件逻辑。技巧: 提取类 + 使用组合。
空对象模式
使用默认对象替换空值检查。异味: 反复出现 if (x == null) 检查。技巧: 引入空对象。
边缘情况与注意事项
| 场景 | 处理方式 |
|---|---|
| 不存在测试 | 先编写特征测试。使用各种输入运行代码,并记录输出。这些测试是你的安全网。 |
| 重构导致远处的测试失败 | 首先弄清楚原因。也许该测试依赖于实现细节。如果是这样,就修改测试,让它测试行为而不是实现。然后继续。 |
| 用户希望同时改变行为并重构 | 拒绝。分开进行。先重构,让行为变更更容易,提交后再改变行为。 |
| 方法过于复杂,难以逐步分析 | 使用“将方法替换为方法对象”。将整个方法转换为一个类,以便提取其中的每个步骤。 |
| 在大型代码库中重构 | 先提取微服务或模块边界,然后在该边界内重构。“除了重构过多,没有什么是无法通过重构解决的。” |
| IDE 提供自动重构功能 | 使用它。现代 IDE 可以安全地重命名、提取方法、引入变量等。只有在 IDE 无法做到时才手动操作。 |
| 需要撤销 | 使用 git stash 或 git reset --hard 回到上次提交。小步提交能让这件事轻松许多。 |
资源
- Martin Fowler,《重构:改善既有代码的设计》(第 2 版,2018 年)
- refactoring.com — Fowler 的在线目录
- refactoring.guru — 图解重构模式
- refactoring.guru/refactoring/catalog — 完整的技术目录(6 个类别、66 种技术)以及本技能所参照的代码异味分类