大仓库重构
何时用
- 需要对大型或老代码库做结构性调整:抽函数、移动模块、重命名、拆分大文件。
- 已有功能运行正常,但代码组织混乱、可读性差,需要在不改变外部行为的前提下清理内部结构。
- 接到"把这个模块重构一下"或"把这些文件整理整理"的任务,改动会波及多个文件或多处调用方。
- 团队准备迁移到新架构,需要分阶段、可回退地把现有代码逐步搬过去。
核心规则
1. 先立测试护栏
规则: 确认关键路径有测试覆盖再动手重构;若测试不存在或覆盖不足,先补特征测试(characterization test)锁住当前行为,再开始结构调整。
为什么: AI 做重构时最常见的事故不是"改错了逻辑",而是"改完觉得没问题,其实已经悄悄改变了行为"。没有测试护栏,重构后的代码和重构前的代码在输入输出上是否完全一致,只能靠肉眼比对,这在大仓库里几乎不可能做到。特征测试的目的不是验证"代码应该做什么",而是记录"代码现在实际做什么"——包括那些可能是 bug 的行为,先把它们冻结下来,重构完再讨论要不要修。
怎么做:
- 重构前先跑现有测试套件,确认全部通过,建立基准绿灯。
- 找出重构目标的关键输入输出(函数签名、HTTP 响应、数据库写入格式),用现有输出写断言,不是写"预期应该怎样"而是写"现在实际是怎样"。
- 特征测试不需要优雅,只需要覆盖:把现有行为的真实输出直接复制进断言,宁可多写几个边界用例。
- 补完测试,跑一遍确认全绿,再切到重构任务。
2. 行为不变原则
规则: 重构只允许改代码结构,不允许同时改外部可见行为;每完成一个重构步骤立刻跑测试,确认行为与改前完全一致。
为什么: AI 在重构时极容易把"顺手改进逻辑"和"结构调整"混在一起——改参数顺序"因为这样更合理",修掉一个"顺眼的边界条件",调整返回值结构"更符合最佳实践"。每一处单独看都是好意,但混在重构里有两个危险:一,下游调用方可能依赖这个"不合理"的行为;二,一旦测试挂掉,无法判断是结构改动破坏的还是逻辑改动破坏的。重构和行为修改必须是两次独立的提交。
怎么做:
- 每个重构步骤结束后立刻跑测试,不要攒几步再跑。
- 若发现现有代码有 bug 或值得改进的逻辑,记录下来,不在当前重构 PR 里动,单独开任务处理。
- 提交信息中明确写"refactor: 只改结构,行为不变",让 reviewer 知道这一步不含逻辑变更,可以用更轻量的方式审查。
- 若测试挂掉,先还原到上一步绿灯状态,找出哪一处结构改动影响了行为,再修正,不要在挂掉状态下继续推进。
3. 单一变换,每步可独立回退
规则: 每次提交只做一种重构操作——改名就只改名,抽函数就只抽函数,移动文件就只移动文件;绝不在同一个提交里混合多种变换。
为什么: AI 被要求"重构这个模块"时,会倾向于一口气生成"改完的最终状态":函数名换了、目录结构调了、逻辑抽象层加了、import 路径更新了——全在一个 diff 里。这类大爆炸式提交在代码 review 时几乎无法有效审查,出问题时更无从用 git bisect 定位。出了问题只能整体回滚,等于白做。单一变换的核心价值是:每一步都可以被精确撤销,问题定位可以缩到一次提交粒度。
怎么做:
- 把重构计划拆成操作列表,每条对应一个提交:
步骤 1: 把 processOrder 里的金额计算逻辑抽成 calculateAmount 函数 步骤 2: 把 calculateAmount 移到 src/domain/pricing.ts 步骤 3: 将所有调用方的 import 路径更新为新路径 步骤 4: 把 processOrder 重命名为 handleOrderSubmission - 每步提交前跑测试确认绿灯,再提交,再进行下一步。
- 若某步执行中发现需要同时改另一件事,先把当前步骤提交或暂存,再另起一个步骤。
4. 自动化优先,不手动批量改
规则: 能用 IDE 重构工具、代码迁移脚本(codemod)、语言服务器安全执行的重构,不要用手写替换或正则批量改;手改正则是大仓库重构引入 bug 的重灾区。
为什么: AI 遇到"把全仓库 500 处 getUserById 改名成 findUserById"时,最容易的做法是写个正则替换脚本或直接生成修改后的文件列表。问题是:正则不理解语法,会改到字符串字面量、注释、不同作用域下同名但含义不同的函数;手工批量替换会跳过动态调用(obj[methodName]())、装饰器、反射场景。IDE 的语言感知重命名会处理这些边界情况,而 AI 生成的正则脚本几乎不会。
怎么做:
- 重命名类/函数/变量:优先用 IDE 的"Rename Symbol"(VSCode F2、IntelliJ Shift+F6),它走语言服务器,能处理所有引用,包括动态调用和类型声明。
- 大规模迁移(如 API 升级、框架切换):优先找官方或社区提供的 codemod 工具(如
jscodeshift、ts-migrate),它们对语法树操作,比正则安全。 - 确实需要脚本替换时:先在小范围(单个文件)跑,人工核对输出正确后再扩大范围,绝不一次性盲跑全仓库。
- 批量改完后,用 Grep 抽查几个典型调用点,确认改动符合预期,没有误改。
5. 改前做影响面分析,大改先对齐团队
规则: 动手前用搜索工具把所有调用点、跨模块引用、对外接口全部找出来;影响面超出单个模块时,先把重构范围和计划同步给团队,拿到共识再动手。
为什么: AI 被要求重构一个函数时,只看得到当前文件,不会主动评估这个函数还被哪些模块依赖、是否有外部包导入、是否被测试 mock 住。常见翻车:把一个看起来"内部"的工具函数改了接口,没发现它被另一个子包直接引用,导致那个子包在 CI 里静默编译失败;或者移动了一个文件,没更新 barrel export,下游的 tree-shaking 路径断了,但不报错只是包变大了,很久后才发现。对于影响面大的重构,团队对齐还有另一个作用:避免和并行开发的同事产生大量合并冲突。
怎么做:
- 改动前用 Grep 搜索目标符号名,确认全部调用点的位置和数量。
- 检查是否有对外导出(barrel index 文件、package.json
exports字段、公共 API 文档),若有则影响面包含外部使用者。 - 画出依赖链:「修改 A → 影响 B、C 模块 → B 被 D 依赖 → 需同步检查 D」,遇到不确定的节点就继续往下追。
- 若影响超过 3 个模块或涉及对外接口,先在 PR 描述或设计文档里写清楚重构计划,让团队 review 计划再 review 代码。
正例 / 反例
反例:大爆炸式重构,一次改几十个文件,测试全红无法定位
# 反例 — AI 一次性"重构完成",单个 PR 包含:
# - 重命名了 12 个函数
# - 把 3 个文件合并成 1 个
# - 抽了 2 个新的抽象类
# - 顺手修了 2 个"看起来不对"的逻辑
# - 更新了所有 import 路径
git log --oneline
# a1b2c3d refactor: 重构 order 模块
# 跑测试结果:
# FAIL src/order/order.test.ts
# FAIL src/payment/payment.test.ts
# FAIL src/notification/notification.test.ts
# 7 个测试挂掉,但不知道哪一步改坏的
# git bisect 无从下手——只有一个提交
# 只能整体回滚,重构归零
正例:单一变换、每步绿灯、可精确二分回退
# 正例 — 同样的重构目标,拆成独立的小步骤
git log --oneline
# f6e5d4c refactor: 把 calculateAmount 从 processOrder 中抽出为独立函数
# e3d2c1b refactor: 把 calculateAmount 移动到 src/domain/pricing.ts
# b9a8f7e refactor: 更新所有调用方 import 路径指向新位置
# 9c8b7a6 refactor: 把 processOrder 重命名为 handleOrderSubmission
# 每步提交前都跑过测试:
# 步骤 1 后 → 全绿 ✓
# 步骤 2 后 → 全绿 ✓
# 步骤 3 后 → 全绿 ✓
# 步骤 4 后 → 发现 2 个测试挂掉 ← 定位精确,只需检查重命名这一步
# git bisect 直接指向步骤 4,发现遗漏了一处动态调用:
# const handler = obj['processOrder']; ← 字符串没跟着改
# 修复一行,重新提交,全绿
两者的本质差异:
| 维度 | 反例(大爆炸) | 正例(小步单一) |
|---|---|---|
| 单次提交改动量 | 几十个文件,多种变换混合 | 一种变换,改动可独立描述 |
| 测试挂掉时定位 | 无从下手,7 个测试挂掉不知原因 | 精确到某一步,范围可控 |
| 回退成本 | 整体回滚,重构归零 | 只回退出问题那一步,其余保留 |
| Code review 效率 | Reviewer 无法判断哪些是结构变动哪些是逻辑变动 | 每个 commit 目的单一,可快速审查 |
自查清单
- 重构前已有测试覆盖关键路径,或已补写特征测试锁住现有行为,跑过一遍确认全绿。
- 本次重构没有混入任何逻辑修改——若发现 bug,已另记为独立任务,没有在此处顺手修。
- 每个提交只包含一种重构操作(改名 / 抽函数 / 移文件),操作类型在提交信息中已明确说明。
- 重命名或移动前已用 Grep 找出全部调用点,没有遗漏的引用位置。
- 对批量改动(重命名、路径迁移)优先使用了 IDE 或 codemod 工具,而非手写正则脚本。
- 若影响面超过单个模块或涉及对外接口,已先同步团队并拿到共识,再开始动手。
- 每个重构步骤完成后都跑了测试,确认绿灯才继续下一步,没有攒步骤一起验证。