重构既有代码(Refactoring)
核心理念
重构是在不改变代码可观察行为的前提下,调整代码结构,使其更易理解、更易修改。
两条铁律:
- 小步前进:每次只做一个微小改动(通常 3~8 分钟),改完立即验证;任何时刻代码都处于可工作状态,随时可以停。
- 依赖于测试:看到红条绝不重构;测试失败且无法立即定位时,回滚到上一个绿色提交,换更小的步子重做。
好代码的检验标准只有一个:人们是否能轻而易举地修改它。
> 本技能提炼自《重构:改善既有代码的设计(第二版)》(中文版)全书 12 章,内容自足、可独立安装。
> 手法名后括号数字为原书页码,仅供人工溯源——执行时不要尝试去读原书文件。
工作流程
第 1 步:圈定扫描范围(默认只扫增量改动)
用户已明确指定目标("重构这段代码/这个文件")时,以用户指定为准,跳过本步。
未指定目标时,默认只扫描"用户自己动过的代码"(营地法则:重构你触碰的地方),不惊动全仓库:
- 用 git 确定增量范围(相对上游分支,同时涵盖未提交与已提交未推送):
bash git diff --name-only @{u} # 增量文件清单(工作区 vs 上游) git ls-files --others --exclude-standard # 未跟踪的新文件 git log --oneline @{u}..HEAD # 增量提交清单(简报用)
无上游分支时退回git diff --name-only origin/main(或origin/master)。 - 向用户简报范围:本次覆盖了哪几个提交、哪些文件。
- 后续的坏味道诊断与重构默认只针对该范围进行。
- 每次报告完增量扫描/重构结果后,主动提醒:"本次仅覆盖你的增量改动,如需全仓库坏味道扫描请告知。"(全仓库扫描应分模块进行,避免一次铺开过大。)
例外处理:
- 非 git 仓库:无法增量定位,请用户指定文件/目录,或征得同意后全仓库扫描。
- 工作区干净且无未推送提交:没有增量可扫,直接询问是否做全仓库扫描。
第 2 步:明确意图,建立基线
- 先想清楚为什么要重构:预备性重构(为加新特性铺路)?帮助理解(读懂代码并把理解写回代码)?捡垃圾(路过顺手清理)?
- 重构与加功能是两顶帽子:绝不混在同一个提交里。
- 通读目标代码,理解它现在做了什么(而不是它应该做什么)。
- 安全网(详见
references/testing.md): - 无测试 → 先为现有行为补特征测试(随便填期望值 → 用实际值回填 → 故意引入错误确认会失败 → 恢复)。
- 有测试 → 先跑一遍确认全绿。
- 实在无法测试 → 至少明确验证手段(编译、运行样例),且只做低风险手法。
第 3 步:诊断坏味道
读取 references/smells.md,对照第 1 步圈定范围内的代码识别坏味道。
记住何时不重构:这段代码不会再被修改;丑代码藏在稳定 API 之下;重写比重构更容易。
第 4 步:提出重构方案,等待用户决策(执行前必经关口)
没有用户批准,不得改动任何一行代码。 诊断完成后,向用户提交一份重构方案并等待确认:
- 发现清单:每个坏味道——在哪个文件/函数、什么味道(引用 smells.md 条目)、严重程度、为什么值得修。
- 建议手法:每个坏味道对应的手法(引用 catalog 条目名),按影响排序,标出建议先做的一个。
- 风险评估:哪些手法是纯局部的(低风险),哪些会触碰公开接口/调用方/数据结构(高风险);测试覆盖是否足以支撑。
- 决策请求,至少包含:
1. 范围:只处理建议的第一项?处理全部低风险项?还是全部?
2. 深度:只做局部整理(改名、提炼、内联),还是允许结构调整(拆类、搬移、多态化)?
3. 约束确认:是否有"不动"的代码(生成代码、稳定 API、热路径)?
用户批准前只读不动。用户砍掉的项目记录在案,收尾时汇报;用户改变范围时按新范围重出简版方案。
第 5 步:按批准的方案执行
按用户批准的范围与深度,对每一项:读取对应 catalog 文件,按"做法"小步清单执行。
每个手法条目沿用原书五段式:名称 → 速写 → 动机(何时做/何时不做)→ 做法 → 范例。
本 skill 的 catalog 压缩了"动机 + 做法",已足以独立支撑执行;条目中的章号与页码是引用信息(供人工溯源),不是文件路径,不要试图去打开它们。
| 手法集 | Reference | 原书 | 典型场景 |
|---|---|---|---|
| (原则与心态) | references/principles.md |
ch1、ch2 | 定义、两顶帽子、何时重构、性能、YAGNI |
| (测试安全网) | references/testing.md |
ch4 | 补测试、边界条件、红绿纪律 |
| 坏味道 | references/smells.md |
ch3 | 24 种坏味道 → 首选重构映射 |
| 第一组重构 | references/catalog-basic.md |
ch6 | 提炼函数(106)/变量(119)、内联(115/123)、改变函数声明(124)、封装变量(132)、引入参数对象(140)、组合成类(144)/变换(149)、拆分阶段(154) |
| 封装 | references/catalog-encapsulation.md |
ch7 | 封装记录(162)/集合(170)、以对象取代基本类型(174)、以查询取代临时变量(178)、提炼类(182)、隐藏委托(189)/移除中间人(192)、替换算法(195) |
| 搬移特性 | references/catalog-moving-features.md |
ch8 | 搬移函数(198)/字段(207)/语句(213/217)、移动语句(223)、拆分循环(227)、以管道取代循环(231)、移除死代码(237) |
| 组织数据 | references/catalog-data.md |
ch9 | 拆分变量(240)、字段改名(244)、以查询取代派生变量(248)、引用↔值对象(252/256) |
| 条件逻辑 | references/catalog-conditionals.md |
ch10 | 分解条件(260)、合并条件(263)、卫语句(266)、以多态取代条件(272)、引入特例(289)、引入断言(302) |
| API | references/catalog-api.md |
ch11 | 查询/修改分离(306)、函数参数化(310)、移除标记参数(314)、保持对象完整(319)、查询↔参数(324/327)、移除设值函数(331)、工厂函数(334)、命令↔函数(337/344) |
| 继承 | references/catalog-inheritance.md |
ch12 | 函数/字段上移下移(350-361)、以子类取代类型码(362)、移除子类(369)、提炼超类(375)、折叠继承体系(380)、以委托取代子/超类(381/399) |
第 6 步:小步执行循环
选取一个坏味道 → 查手法要点 → 应用微小改动 → 编译 → 测试 → 提交
- 只执行方案获批范围内的手法;执行中发现新的坏味道或更大的结构问题 → 不要顺手扩大战场,记录下来,纳入下次方案再请用户决策。
- 每步绿色就 commit;重构过程中代码行数可能增加(包装成本),这是用明确性换简洁性,正常。
- 名录原则:一个重构并非只有一套做法,熟练后可调整;情况越复杂,步子要越小。
- 常见手法链(详见
references/principles.md): - 长函数:找注释定位 → 提炼函数 → 改名 → 移除临时变量(以查询取代/内联)→ 需要复用再拆分阶段。
- 类型码分支散布多处:建类搬计算 → 工厂函数按类型返回子类 → 多态取代条件表达式。
- 组织不良的一组函数:先内联成一大块,再按更好的边界重新提炼。
第 7 步:收尾
- 复查公开接口是否需同步(调用方、文档、已发布接口要保留旧声明并 deprecated)。
- 向用户汇报:批准方案中各项的完成情况(含被砍掉/未做的项)、识别了哪些坏味道、应用了哪些手法、行为不变的依据(测试结果)、后续建议(含执行中新发现、留待下次决策的项)。
- 若本次走的是默认增量范围,收尾时再次提醒可做全仓库扫描。
- 营地法则:让代码库比来时更健康,但不必追求完美——在"继续重构"与"回去干活"之间平衡。
原则速查(详见 references/principles.md)
- 两顶帽子:重构时不加功能,加功能时不重构;频繁换帽子没关系,但要清楚戴着哪顶。
- 三次法则:事不过三,三则重构;不要过早抽象(YAGNI)。
- 性能:先重构后调优;调优靠度量找热点(程序大半时间耗在一小半代码上),不靠臆测。
- 命名是核心产出:想不出好名字,往往潜藏更深的设计问题。
- 不可变性是防腐剂:能不可变就不可变;可变数据作用域越小越好。
- 先封装再搬移:数据没有转发机制,先封装变量把"重组数据"化为"重组函数"。
- 重构由经济利益驱动(开发更快),不是道德洁癖。
Skill 维护约定
本 skill 由逐章精读《重构(第二版)》提炼而成(ch1~ch12 全部完成)。改进时:
- 修正/增补条目请保持"动机 + 做法"结构和原书页码锚点;
- 与原书冲突时以原书为准,标注章节便于回查(开发仓库的 knowledgebase/ 有原书全文,仅作校对用);
- 可移植性约束:skill 内不得引用本仓库的文件路径(如 knowledgebase/)——用户安装时只带走 skill/ 目录;引用一律只保留章号与页码。