mcpField
R
refactor

refactor

github.com/smallnest开发298526 次安装7 周前更新
代码仓库

关于 refactor

refactor 是依据 Martin Fowler 重构目录整理代码的技能,面向需要改善代码结构、可读性和可维护性的开发者。它强调保持外部行为不变,以小步修改和测试验证为基础。

主要功能

  1. 01依据代码异味选择重构方法
  2. 02提供多类 Fowler 重构技术
  3. 03要求保持外部行为不变
  4. 04强调小步修改与测试验证
  5. 05通过决策卡评估重构依据与成效

适用场景

  1. 01拆分过长的方法或过大的类
  2. 02消除重复代码并调整职责归属
  3. 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 的目录。每项技术都包含其机械步骤。

组合方法
提炼方法

将代码片段转换为一个方法,并用能说明其用途的名称命名。

步骤:

  1. 根据代码片段的作用(而非实现方式)为新方法命名
  2. 将要提炼的代码复制到新方法中
  3. 确定局部变量:只读变量变为参数,已修改的变量变为返回值
  4. 传入参数并处理返回值
  5. 将原代码片段替换为对新方法的调用
  6. 测试

之前:

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);
}
内联方法

当方法体和方法名一样清晰时,用方法体替换方法调用。

步骤:

  1. 确认该方法不是多态方法(没有子类重写它)
  2. 找出所有调用方
  3. 将每处调用替换为方法体
  4. 删除方法定义
  5. 测试
提炼变量

将表达式(或表达式的一部分)的结果赋给一个自我说明的变量。

之前:

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) {
    // ...
}
内联临时变量

当临时变量只使用一次,且表达式清晰时,用表达式替换临时变量。

以查询取代临时变量

将表达式提炼为方法。对于只计算一次并重复使用的临时变量,用方法调用替换。

分解临时变量

对赋值多次的临时变量(循环或累积用变量除外),应按职责拆分为多个独立变量。

移除对参数的赋值

不要对参数赋值,改用局部变量。

以方法对象取代方法

当长方法使用许多局部变量,导致难以提炼方法时,将该方法转换为独立的类,并将局部变量作为字段。

替换算法

用更清晰的算法替换现有算法。


在对象之间搬移特性
移动方法

将方法移到最常使用它的类中。

机制:

  1. 检查该方法在当前类中使用的所有特性
  2. 检查是否存在多态(子类/超类方法)
  3. 在目标类中创建该方法,并根据需要进行调整
  4. 从源对象引用目标对象
  5. 将源方法改为委托方法,或将其删除
  6. 测试
移动字段

将字段移到最常使用它的类中。

提取类

当一个类承担了两类工作时,将它拆分。创建一个新类,并将相关字段和方法移过去。

内联类

当一个类几乎什么也不做时,将它合并到最常使用它的类中。

隐藏委托

在服务端创建方法,隐藏委托链。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:准备
  1. 如果尚不存在,编写特征测试。这些测试用于记录当前行为——不必优雅,只需足够全面,能够发现回归问题。
  2. 为所声称的问题记录基线:记录有代表性的行为、改动范围、相关调用方,或性能工作的基准测试/性能分析/查询次数。没有性能基线时,应将性能想法描述为候选方案,而不是已测得的改进。
  3. 提交当前状态。从干净的工作树开始。
  4. 为重构创建分支。将其与功能开发分开。
阶段 2:识别
  1. 嗅探代码异味。 使用上方的异味目录对问题进行分类,将阈值视为候选信号。
  2. 理解代码。 彻底阅读代码,梳理相关调用方和依赖项,并找出具体的故障或变更场景。
  3. 选择一项主要原则。 仅将相关原则作为解释;不要将一个根本原因拆分成多个发现。
  4. 检查制约因素。 确认 DRY 不会造成虚假抽象,OCP/DIP 并非猜测,SRP 不会产生仅转发调用的代码片段,并且组合确实能解决继承耦合问题。
  5. 选择合适的重构方式。 从目录中选择最小的技术方案,并定义如何检查基线和行为。
阶段 3:重构(小步进行)

每一步:

  1. 进行一项小改动。 解决主要异味中一个可验证的部分;不要捆绑无关的原则清理工作。
  2. 编译。 每次改动后,代码都应能编译。
  3. 运行测试。 所有测试都必须通过。如果未通过,说明你改变了行为。
  4. 检查接缝。 新接口、适配器或间接层必须能消除调用方的实际复杂性,或支持现有变化;否则,保留更简单的设计。
  5. 提交。 创建一条类似 refactor: extract validateEmail method 的提交说明。

重复以上步骤,直到异味解决。

阶段 4:验证
  1. 行为。 所有测试、类型检查、编译和手动冒烟测试均通过。
  2. 结构。 对照决策卡,比较依赖方向、相关调用方、重复知识或变更范围。
  3. 快速失败语义。 如果验证提前执行,须保留错误类型/代码、聚合、重试、事务和清理行为。
  4. 性能。 如果性能是改动依据,请在相同的工作负载和环境下重新运行。报告结果和噪声范围;如果没有基线,只报告未观察到明显退化。
  5. 差异审查。 检查是否存在非预期改动,并确认已满足成功条件。
阶段 5:清理
  1. 删除过时注释。 如果重构后某条注释变得显而易见,请删除它。
  2. 检查死代码。 重构后可能会出现未使用的代码。
  3. 移除推测性接缝。 删除没有当前实际价值的单实现接口、仅转发调用的模块或未使用的扩展点。
  4. 最终提交。 总结重构步骤。

重构检查清单

代码质量
  • 正在处理一个主要异味/根本原因;相关原则不作为单独工作项计算
  • 已记录证据、预期影响、基线和成功条件
  • 函数大小适合其上下文(行数是候选信号,而非限制)
  • 在有助于明确职责边界时,每个函数只做一件事(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 链
  • 实现 From trait 可简化类型转换
  • 派生宏可减少样板代码

常见重构步骤

提取方法步骤
  1. 创建一个以意图命名的新方法
  2. 将代码片段复制到新方法中
  3. 确定局部变量 → 参数/返回值
  4. 在原位置调用新方法
  5. 测试
使用多态替换条件语句步骤
  1. 为每种变体创建子类
  2. 创建一个返回正确子类的工厂方法
  3. 将条件语句主体移至相应的子类方法中
  4. 删除条件语句
提取类步骤
  1. 确定一组连贯的字段和方法
  2. 创建一个新类
  3. 在旧类中创建新类的实例
  4. 一次移动一个字段和方法
  5. 更新旧类中的引用
  6. 每次移动后都进行测试
内联类步骤
  1. 确定目标类的所有调用方
  2. 将所有方法/字段移至接收类
  3. 将所有引用重定向到接收类
  4. 删除空类
  5. 测试

安全流程

开始操作之前
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 回到上次提交。小步提交能让这件事轻松许多。

资源

评论

支持 Markdown