# Large Repo Refactor

> 在大型存量代码库做重构时使用。控制影响面,小步推进,不破坏现有行为。

- Skill: `wade-devcode/large-repo-refactor` (Agent Skill)
- Install (CLI): `npx skillmds@latest add wade-devcode/large-repo-refactor`
- Raw SKILL.md: https://api.skillmd.com/api/skills/wade-devcode/large-repo-refactor/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Wade-DevCode (https://skillmd.com/u/wade-devcode)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/wade-devcode/large-repo-refactor

---


# 大仓库重构

## 何时用

- 需要对大型或老代码库做结构性调整：抽函数、移动模块、重命名、拆分大文件。
- 已有功能运行正常，但代码组织混乱、可读性差，需要在不改变外部行为的前提下清理内部结构。
- 接到"把这个模块重构一下"或"把这些文件整理整理"的任务，改动会波及多个文件或多处调用方。
- 团队准备迁移到新架构，需要分阶段、可回退地把现有代码逐步搬过去。

## 核心规则

### 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 工具，而非手写正则脚本。
- [ ] 若影响面超过单个模块或涉及对外接口，已先同步团队并拿到共识，再开始动手。
- [ ] 每个重构步骤完成后都跑了测试，确认绿灯才继续下一步，没有攒步骤一起验证。

