目录
- [1. 引言](#1. 引言)
- 重构闭环全景
- [2. 重构前的评估:先搞清楚要面对什么](#2. 重构前的评估:先搞清楚要面对什么)
- 工具链与替代方案对比
- [3. 制定重构策略:分层推进,降低风险](#3. 制定重构策略:分层推进,降低风险)
- [4. 用 Claude Code 执行重构:核心工作流](#4. 用 Claude Code 执行重构:核心工作流)
- [5. 验证与回归:确保重构不破坏功能](#5. 验证与回归:确保重构不破坏功能)
- [6. 常见陷阱与应对](#6. 常见陷阱与应对)
- [7. 总结](#7. 总结)
- [8. 参考资料](#8. 参考资料)
TL;DR
- 评估:代码库体检、依赖耦合分析,输出按风险排序的清单
- 规划:明确目标与成功标准,按模块划分批次并预留回滚点
- 执行:先只读分析,再单点改造,每步立即跑测试验证
- 验证:自动化测试兜底,对比重构前后输出与性能指标
- 底线:小步提交 + 人工审查,让每次改动可解释、可回退
1. 引言
遗留系统(Legacy System)是许多技术团队绕不开的难题:代码耦合严重、文档缺失、测试覆盖不足、新人上手成本高。传统重构往往需要投入大量人力,且风险难以控制。而 Claude Code 作为一款 AI 编程助手,能够在理解代码库的基础上辅助完成分析、规划、编码与验证,为遗留系统重构提供了一条更高效、更可控的路径。
本文将从评估、规划、执行到验证,系统梳理用 Claude Code 重构遗留系统的完整方法论,并给出可落地的操作建议与注意事项。
重构闭环全景
下图展示了用 Claude Code 重构遗留系统的完整闭环:评估 → 规划 → 小步执行 → 验证 → 回滚,验证通过则进入下一批次,验证失败则回滚后重新规划。
#mermaid-svg-sMc9qI0FSVTKO8sH{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sMc9qI0FSVTKO8sH .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sMc9qI0FSVTKO8sH .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sMc9qI0FSVTKO8sH .error-icon{fill:#552222;}#mermaid-svg-sMc9qI0FSVTKO8sH .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sMc9qI0FSVTKO8sH .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sMc9qI0FSVTKO8sH .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sMc9qI0FSVTKO8sH .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sMc9qI0FSVTKO8sH .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sMc9qI0FSVTKO8sH .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sMc9qI0FSVTKO8sH .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sMc9qI0FSVTKO8sH .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sMc9qI0FSVTKO8sH .marker.cross{stroke:#333333;}#mermaid-svg-sMc9qI0FSVTKO8sH svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sMc9qI0FSVTKO8sH p{margin:0;}#mermaid-svg-sMc9qI0FSVTKO8sH .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-sMc9qI0FSVTKO8sH .cluster-label text{fill:#333;}#mermaid-svg-sMc9qI0FSVTKO8sH .cluster-label span{color:#333;}#mermaid-svg-sMc9qI0FSVTKO8sH .cluster-label span p{background-color:transparent;}#mermaid-svg-sMc9qI0FSVTKO8sH .label text,#mermaid-svg-sMc9qI0FSVTKO8sH span{fill:#333;color:#333;}#mermaid-svg-sMc9qI0FSVTKO8sH .node rect,#mermaid-svg-sMc9qI0FSVTKO8sH .node circle,#mermaid-svg-sMc9qI0FSVTKO8sH .node ellipse,#mermaid-svg-sMc9qI0FSVTKO8sH .node polygon,#mermaid-svg-sMc9qI0FSVTKO8sH .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sMc9qI0FSVTKO8sH .rough-node .label text,#mermaid-svg-sMc9qI0FSVTKO8sH .node .label text,#mermaid-svg-sMc9qI0FSVTKO8sH .image-shape .label,#mermaid-svg-sMc9qI0FSVTKO8sH .icon-shape .label{text-anchor:middle;}#mermaid-svg-sMc9qI0FSVTKO8sH .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sMc9qI0FSVTKO8sH .rough-node .label,#mermaid-svg-sMc9qI0FSVTKO8sH .node .label,#mermaid-svg-sMc9qI0FSVTKO8sH .image-shape .label,#mermaid-svg-sMc9qI0FSVTKO8sH .icon-shape .label{text-align:center;}#mermaid-svg-sMc9qI0FSVTKO8sH .node.clickable{cursor:pointer;}#mermaid-svg-sMc9qI0FSVTKO8sH .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sMc9qI0FSVTKO8sH .arrowheadPath{fill:#333333;}#mermaid-svg-sMc9qI0FSVTKO8sH .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sMc9qI0FSVTKO8sH .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sMc9qI0FSVTKO8sH .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sMc9qI0FSVTKO8sH .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sMc9qI0FSVTKO8sH .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sMc9qI0FSVTKO8sH .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sMc9qI0FSVTKO8sH .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sMc9qI0FSVTKO8sH .cluster text{fill:#333;}#mermaid-svg-sMc9qI0FSVTKO8sH .cluster span{color:#333;}#mermaid-svg-sMc9qI0FSVTKO8sH div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-sMc9qI0FSVTKO8sH .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sMc9qI0FSVTKO8sH rect.text{fill:none;stroke-width:0;}#mermaid-svg-sMc9qI0FSVTKO8sH .icon-shape,#mermaid-svg-sMc9qI0FSVTKO8sH .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sMc9qI0FSVTKO8sH .icon-shape p,#mermaid-svg-sMc9qI0FSVTKO8sH .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sMc9qI0FSVTKO8sH .icon-shape .label rect,#mermaid-svg-sMc9qI0FSVTKO8sH .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sMc9qI0FSVTKO8sH .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sMc9qI0FSVTKO8sH .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sMc9qI0FSVTKO8sH :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 通过
失败
评估:代码库体检与风险清单
规划:目标、批次与回滚点
小步执行:单点改造 + 即时验证
验证:测试与对比是否通过
合并提交,进入下一批次
回滚:revert 单次提交
各阶段的核心产出物:评估 阶段输出模块依赖图与按风险排序的清单;规划 阶段产出重构目标、批次划分与回滚方案;小步执行 阶段每次只交付一个可验证的改动及其测试;验证 阶段给出测试报告与前后对比结果;回滚阶段则留下可追溯的反向提交与改动说明,供复盘与重新规划。
2. 重构前的评估:先搞清楚要面对什么
在动手重构之前,先让 Claude Code 帮你摸清遗留系统的真实状况。这一阶段的目标是建立对系统的整体认知,识别高风险区域。
2.1 代码库体检
- 让 Claude Code 扫描项目结构,生成模块依赖关系图
- 统计代码行数、文件数量、语言分布
- 识别重复代码、死代码、超长函数等坏味道
2.2 依赖与耦合分析
- 梳理外部依赖(第三方库、服务、数据库)
- 识别模块间的循环依赖与紧耦合点
- 标记不可测试的代码区域
2.3 风险清单输出
让 Claude Code 基于分析结果输出一份风险清单,按影响面与改动难度排序,作为后续规划的输入。
工具链与替代方案对比
Claude Code 并不是要取代人工重构或传统静态分析工具,三者擅长的事情并不相同。下面按「评估 → 改造 → 验证」三个阶段,对比它们在遗留系统重构中的适用场景与局限。
| 阶段 | 方案 | 适用场景 | 局限 |
|---|---|---|---|
| 评估 | Claude Code | 快速通读陌生代码库,生成模块依赖图、业务语义摘要与按风险排序的清单;能解释「这段代码在做什么」 | 结论依赖上下文质量,大仓库需分批喂入;对运行时行为、真实调用量无感知 |
| 评估 | 人工重构 | 判断业务价值、历史包袱与团队约束,决定哪些模块值得改、哪些必须保留 | 耗时最长,依赖资深成员经验,难以覆盖全量代码 |
| 评估 | SonarQube / jscpd | 规则化、可量化的体检:圈复杂度、重复率、坏味道、安全漏洞,结果稳定可对比 | 只报「哪里有问题」,不解释业务含义,也不给改造方案;规则外的隐性耦合容易漏掉 |
| 改造 | Claude Code | 单点抽取、重命名、补测试、生成改动说明,适合小步、可验证的机械性改造 | 可能「过度自信」地改动语义,必须人工审查;跨文件大范围重构容易失控 |
| 改造 | 人工重构 | 涉及架构调整、领域模型重塑、跨团队接口变更等需要权衡的决策 | 人力成本高、周期长,容易一次性改动过大 |
| 改造 | SonarQube / jscpd | 不参与改造,仅作为改造后的回归门禁(质量阈值卡点) | 无法自动修复,只能提示 |
| 验证 | Claude Code | 生成特征测试与单元测试、对比重构前后输出、总结 diff 风险点 | 生成的测试需人工确认断言是否真的锁定「现状」 |
| 验证 | 人工重构 | 关键路径的代码审查、边界条件与异常语义的最终确认 | 覆盖有限,难以逐行吃透大 diff |
| 验证 | SonarQube / jscpd | 持续集成中做质量门禁:重复率下降、复杂度不上升、无新增严重问题 | 只反映静态指标,无法证明行为等价 |
三者组合使用的建议:
- 评估阶段以工具打底、AI 补语义:先用 SonarQube、jscpd 跑出一份客观的量化报告(复杂度、重复率、坏味道分布),再让 Claude Code 结合这份报告阅读代码,输出「哪些问题值得优先处理、为什么」的业务解释,最后由人工拍板确定重构范围。
- 改造阶段以 AI 执行、人工把关:让 Claude Code 承担抽取、重命名、补测试等机械性工作,每次只改一处并立即提交;关键模块的 diff 必须人工 review,架构级调整仍由人主导。
- 验证阶段以工具守门、AI 补测试:把 SonarQube 的质量阈值接入 CI 作为硬门禁,用 Claude Code 生成特征测试锁定现有语义,两者叠加才能同时保证「指标不退化」和「行为不改变」。
一句话总结:静态分析工具负责「发现问题」,Claude Code 负责「解释与动手」,人工负责「决策与兜底」------三者各司其职,比任何单一方案都更稳。
3. 制定重构策略:分层推进,降低风险
遗留系统重构切忌一步到位,建议采用「分层推进、小步快跑」的策略。
3.1 确定重构目标
- 是提升可维护性、提升性能,还是为后续功能迭代铺路?
- 明确成功标准(如测试覆盖率、模块解耦程度、构建时长)
3.2 划分重构批次
- 按模块或业务域拆分,每个批次独立可交付
- 每个批次包含:分析 → 改造 → 验证 → 回归
3.3 制定回滚方案
- 明确每个批次的回滚点
- 保留可恢复的基线版本
4. 用 Claude Code 执行重构:核心工作流
这一阶段是实践重点,介绍如何让 Claude Code 高效参与具体改造。
4.1 让 Claude Code 理解现状
- 提供清晰的上下文:项目结构、关键入口、业务说明
- 让 Claude Code 先输出它对现有代码的理解,确认后再动手
4.2 小步改造与即时验证
- 每次只让 Claude Code 完成一个小的、可验证的改动
- 每步改动后立即运行测试或编译,确认无回归再继续
实战示例:一次「小步改造 + 即时验证」的完整闭环
假设遗留代码里有两段几乎一样的「订单金额计算」逻辑,分别散落在 OrderService 和 ReportService 中。第一步不是直接改,而是先让 Claude Code 定位重复点:
text
> 扫描 src/main/java 下所有 Java 文件,找出计算订单金额的重复逻辑,
> 列出重复代码所在文件、行号与差异点,先不要修改任何文件。
Claude Code 返回的重复点摘要(示意):
text
发现 2 处重复逻辑:
- OrderService.java:42-58 calcTotal()
- ReportService.java:87-103 calcOrderAmount()
差异:ReportService 版本未处理 null 优惠券,且金额未做四舍五入。
建议:抽取 OrderAmountCalculator 工具类,统一处理 null 与舍入。
确认理解无误后,再让它只做这一处抽取,并立即跑测试:
text
> 按上面的建议抽取 OrderAmountCalculator,保持两个调用方行为一致,
> 改完后运行 mvn -q test -Dtest=OrderServiceTest,ReportServiceTest。
重构前(重复代码)
java
// OrderService.java
public BigDecimal calcTotal(Order order) {
BigDecimal total = BigDecimal.ZERO;
for (Item item : order.getItems()) {
total = total.add(item.getPrice().multiply(BigDecimal.valueOf(item.getQty())));
}
if (order.getCoupon() != null) {
total = total.subtract(order.getCoupon().getAmount());
}
return total.setScale(2, RoundingMode.HALF_UP);
}
// ReportService.java ------ 几乎相同的逻辑,但漏了 null 判断与舍入
public BigDecimal calcOrderAmount(Order order) {
BigDecimal total = BigDecimal.ZERO;
for (Item item : order.getItems()) {
total = total.add(item.getPrice().multiply(BigDecimal.valueOf(item.getQty())));
}
total = total.subtract(order.getCoupon().getAmount());
return total;
}
重构后(抽取公共实现)
java
// OrderAmountCalculator.java
public final class OrderAmountCalculator {
private OrderAmountCalculator() {
}
public static BigDecimal calc(Order order) {
BigDecimal total = BigDecimal.ZERO;
for (Item item : order.getItems()) {
total = total.add(item.getPrice().multiply(BigDecimal.valueOf(item.getQty())));
}
if (order.getCoupon() != null) {
total = total.subtract(order.getCoupon().getAmount());
}
return total.setScale(2, RoundingMode.HALF_UP);
}
}
// OrderService.java
public BigDecimal calcTotal(Order order) {
return OrderAmountCalculator.calc(order);
}
// ReportService.java
public BigDecimal calcOrderAmount(Order order) {
return OrderAmountCalculator.calc(order);
}
运行结果注释
text
$ mvn -q test -Dtest=OrderServiceTest,ReportServiceTest
Tests run: 12, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
# 注意:ReportServiceTest 中原本有一条「无优惠券订单」用例,
# 重构前因 NPE 被 @Disabled 跳过;重构后该用例恢复执行并通过,
# 说明重复逻辑合并顺带修复了一个潜在缺陷。
这一步的关键在于:先只读分析、再单点改造、最后立即验证。改动范围被限制在一个工具类加两个调用方,一旦测试失败,回滚成本极低。
改造失败时怎么回滚
小步改造的价值,不只体现在「改对了」,更体现在「改错了也能快速退回去」。当某次抽取或重构导致测试失败、行为异常时,按下面的顺序处理即可。
1. 用 git revert 撤销单次提交
每次小步改造都单独提交,回滚时只针对那一个 commit,不牵连其他改动:
bash
# 查看最近提交,确认要回滚的那一次
git log --oneline -5
# 生成一条反向提交,撤销指定 commit(保留历史,适合已推送的分支)
git revert 3f9c1a2
# 若尚未推送、想直接丢弃这次提交,也可用 reset(谨慎使用)
git reset --hard HEAD~1
git revert 会新增一条「反向」提交,历史可追溯、对协作分支友好;git reset 则直接改写历史,只适合本地未推送的场景。
2. 保留基线分支,随时可对照
在开始每个批次前打一个基线标签或分支,回滚时既能退回代码,也能对照差异:
bash
# 批次开始前建立基线
git branch baseline/order-refactor-2024
git tag baseline-order-refactor
# 回滚后与基线对比,确认已完全恢复
git diff baseline/order-refactor-2024 -- src/main/java
3. 让 Claude Code 生成改动说明,辅助定位问题
回滚前先搞清楚「这次改动到底动了什么」,避免误伤:
text
> 读取 commit 3f9c1a2 的 diff,用中文总结这次改动:
> 修改了哪些文件、每个文件改了什么、可能影响哪些调用方,
> 并列出最可能导致测试失败的三处风险点。
Claude Code 输出的改动说明可以直接贴进回滚提交信息或 issue,方便团队复盘。
4. 回滚后的验证
bash
git revert 3f9c1a2 --no-edit
mvn -q test
# Tests run: 12, Failures: 0, Errors: 0, Skipped: 0
# BUILD SUCCESS ------ 回到改造前的绿色状态,再重新规划下一步
回滚不是失败,而是小步策略的「安全气囊」:改动越小,回滚越快,团队才敢持续尝试。
两种策略的横向对比
| 对比维度 | 一次性大重构 | 小步改造 + 即时验证 |
|---|---|---|
| 改动范围 | 一次覆盖多个模块,动辄数百上千行,边界难以界定 | 每次只动一个工具类或一两个调用方,范围清晰可控 |
| 回滚成本 | 一旦失败需整体回退,可能牵连已合并的无关改动 | 单点回滚,通常只需 revert 一次小提交,成本极低 |
| 测试覆盖 | 改动面大,测试难以覆盖全部路径,容易留下盲区 | 每步都有针对性测试兜底,覆盖率随批次稳步提升 |
| 风险等级 | 高,问题往往在集成阶段集中暴露,定位困难 | 低,问题在当次改动内即被发现,定位范围小 |
| 人工审查强度 | 审查量大且难以逐行吃透,Review 容易流于形式 | 每次 diff 小、语义集中,审查更细致、更有效 |
结论:一次性大重构看似「一步到位」,实则把风险集中到最后一刻,一旦失败代价高昂;而小步改造配合即时验证,把大风险拆解为一连串可回滚的小风险,每一步都有测试与人工审查兜底。对遗留系统而言,可控性远比速度重要------宁可慢一点,也要让每次改动都「可解释、可验证、可回退」。
4.3 善用 Claude Code 的辅助能力
- 生成单元测试与集成测试
- 自动补充文档与注释
- 识别并消除重复代码
实战示例:用 Claude Code 批量识别重复代码
当重复点不止一处时,可以让 Claude Code 做一次系统性扫描,并输出可执行的改造清单:
text
> 用 jscpd 的思路检查 src 下的重复代码块(阈值 20 行),
> 输出重复片段清单,按「影响文件数 × 重复行数」排序,
> 每条给出建议的抽取方式(工具类 / 父类 / 策略模式)。
Claude Code 输出的清单(示意):
text
| 重复片段 | 文件数 | 重复行数 | 建议抽取方式 |
|---------------------|--------|----------|-------------------|
| 订单金额计算 | 2 | 17 | 静态工具类 |
| 日期区间格式化 | 5 | 12 | 公共 DateUtils |
| 分页参数校验 | 4 | 9 | 父类 BaseController |
随后按清单逐条推进,每条都遵循「抽取 → 跑测试 → 提交」的节奏。Claude Code 还能顺带生成对应的单元测试,把重构后的行为固定下来:
java
class OrderAmountCalculatorTest {
@Test
void shouldIgnoreNullCoupon() {
Order order = new Order(List.of(new Item(new BigDecimal("10.00"), 2)), null);
assertEquals(new BigDecimal("20.00"), OrderAmountCalculator.calc(order));
}
@Test
void shouldRoundHalfUp() {
Order order = new Order(List.of(new Item(new BigDecimal("9.995"), 1)), null);
assertEquals(new BigDecimal("10.00"), OrderAmountCalculator.calc(order));
}
}
text
$ mvn -q test -Dtest=OrderAmountCalculatorTest
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
需要提醒的是,AI 给出的抽取建议仍需人工判断:像「分页参数校验」这类逻辑,抽到父类可能引入继承耦合,改用组合或工具类往往更合适。
4.4 人工审查不可替代
- 关键模块的改动必须人工 review
- 让 Claude Code 输出改动说明,便于审查
4.5 常用提示词模板
前面几节的示例都是「一次性」的,实际项目里更推荐把高频场景沉淀成可复用的提示词模板:把变量抽成占位符,每次只替换 {文件路径}、{方法名} 这类参数,既省去反复组织语言,也能保证每次给 Claude Code 的上下文结构一致、输出更稳定。
| 场景 | 提示词模板 | 使用要点 |
|---|---|---|
| 评估 | > 阅读 {文件路径},先不要修改任何代码。请输出:1. 该文件的职责与关键入口;2. 模块依赖与外部调用方;3. 重复代码、超长函数、死代码等坏味道;4. 按「影响面 × 改动难度」排序的风险清单。 |
明确「先只读、不改动」;要求输出结构化清单而非散文,便于直接作为后续规划的输入 |
| 抽取 | > 将 {文件路径} 中 {方法名} 的重复逻辑抽取为 {目标类名},保持所有调用方行为一致(含 null、舍入、异常语义)。只改这一处,改完运行 {测试命令},并列出改动文件清单。 |
一次只抽一处;把「行为一致」的边界条件写清楚;必须附带验证命令,避免改完不跑测试 |
| 补测试 | > 为 {文件路径} 中的 {方法名} 生成特征测试(characterization test):断言值以当前实际输出为准,不要按你认为正确的业务逻辑写;覆盖 null、空集合、负数与临界阈值;使用 JUnit 5 + @Nested 分组,并标注对应源码行号。 |
强调「锁定现状而非应然」;已知缺陷也要显式锁定并加注释,重构时行为变更才可见 |
| 生成改动说明 | > 读取 commit {commit_id} 的 diff,用中文总结:修改了哪些文件、每个文件改了什么、可能影响哪些调用方,并列出最可能导致测试失败的三处风险点。 |
用于提交信息、PR 描述或回滚前的排查;输出可直接贴进 issue,方便团队复盘 |
| 回滚分析 | > 分析 commit {commit_id} 引入的改动与当前失败用例 {测试类名} 的关系:判断失败是否由该提交导致,给出回滚(git revert)或修复两个方案及各自影响范围,先不要执行任何命令。 |
先判断「是不是这次改动导致的」,再决定回滚还是修复;要求给出方案对比,避免误伤无关改动 |
几点使用建议:
- 占位符要具体 :
{文件路径}尽量写到类文件级别,{方法名}带上参数签名,比笼统说「订单相关代码」效果好得多。 - 模板里固定「先分析、后动手」:评估与回滚分析类模板都显式要求「先不要修改/执行」,避免 Claude Code 直接改代码。
- 把验证命令写进模板 :抽取、补测试类模板都带上
{测试命令},让「改完即验证」成为流程的一部分,而不是靠人记得。 - 模板随项目演进:把团队踩过的坑补进模板(如「注意 BigDecimal 舍入」「不要抽父类」),模板本身就是一份可执行的重构规范。
5. 验证与回归:确保重构不破坏功能
重构的成败最终由验证环节决定。
5.1 自动化测试兜底
- 重构前先补齐关键路径的测试
- 每个批次完成后运行全量测试
重构前如何补齐测试:用特征测试锁定现有语义
遗留系统最常见的困境是「没有测试,不敢改」。此时不要急着写「理想中的测试」,而是先写特征测试(characterization test):不判断现有行为是否「正确」,只把当前行为原样记录下来,作为重构前的语义基线。重构后只要这些测试仍然通过,就说明行为没有被意外改变。
第一步是让 Claude Code 先读懂方法、再生成测试,提示词里要明确「以现有行为为准」:
text
> 阅读 src/main/java/com/example/legacy/PriceCalculator.java 中的
> calcDiscount(Order order, Member member) 方法,先不要修改任何代码。
> 请完成三件事:
> 1. 用中文说明该方法的分支、边界条件与隐含假设(如 null、负数、空集合);
> 2. 为每个分支设计一个特征测试用例,断言值以「当前实际输出」为准,
> 不要按你认为正确的业务逻辑去写;
> 3. 输出 JUnit 5 测试类,使用 @Nested 按分支分组,并标注每个用例
> 对应的源码行号,方便后续对照。
Claude Code 会先给出分支梳理,再产出测试代码。下面是一个典型的特征测试示例:
java
// PriceCalculatorCharacterizationTest.java
class PriceCalculatorCharacterizationTest {
private final PriceCalculator calculator = new PriceCalculator();
@Nested
@DisplayName("calcDiscount ------ 现有行为基线")
class CalcDiscount {
@Test
@DisplayName("普通会员 + 无优惠券:当前返回原价,不打折")
void normalMemberWithoutCoupon() {
Order order = new Order(List.of(new Item(new BigDecimal("100.00"), 1)), null);
// 特征断言:记录当前行为(普通会员不打折),而非期望行为
assertEquals(new BigDecimal("100.00"),
calculator.calcDiscount(order, Member.normal()));
}
@Test
@DisplayName("VIP 会员 + 满 200:当前按 9 折计算并保留两位小数")
void vipMemberOverThreshold() {
Order order = new Order(List.of(new Item(new BigDecimal("99.99"), 3)), null);
assertEquals(new BigDecimal("269.97"),
calculator.calcDiscount(order, Member.vip()));
}
@Test
@DisplayName("优惠券金额大于订单金额:当前返回负数(已知缺陷,先锁定)")
void couponExceedsTotal() {
Order order = new Order(List.of(new Item(new BigDecimal("10.00"), 1)),
new Coupon(new BigDecimal("50.00")));
// 这里刻意锁定「负数」这一不合理行为,重构时若被修正,
// 该用例会失败,从而提醒我们这是一次有意的行为变更
assertEquals(new BigDecimal("-40.00"),
calculator.calcDiscount(order, Member.normal()));
}
@Test
@DisplayName("空商品列表:当前抛 IllegalArgumentException")
void emptyItems() {
Order order = new Order(List.of(), null);
assertThrows(IllegalArgumentException.class,
() -> calculator.calcDiscount(order, Member.vip()));
}
}
}
text
$ mvn -q test -Dtest=PriceCalculatorCharacterizationTest
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0
BUILD SUCCESS
# 说明:4 条用例全部通过,说明它们准确记录了「当前」行为。
# 其中 couponExceedsTotal 锁定的负数结果是一个已知缺陷,
# 重构阶段若顺手修复,该用例会失败------这不是回归,而是有意的行为变更,
# 此时应显式更新断言并在提交信息中说明,而不是悄悄改掉。
几个实践要点:
- 断言「现状」而非「应然」:特征测试的价值在于可比对,写错成理想行为就失去了基线意义。
- 覆盖分支与边界:null、空集合、负数、临界阈值(如满 200 减 20 的 199.99 / 200.00)优先补齐。
- 已知缺陷也要锁定:用注释标明「这是缺陷」,重构时若修复则同步更新断言,让行为变更显式可见。
- 先跑通再重构:这批测试全绿之后再动手改代码,之后每次小步改造都以它为准绳。
5.2 对比验证
- 重构前后对同一输入对比输出
- 关注边界条件与异常处理
5.3 性能与稳定性验证
- 对比重构前后的性能指标
- 观察内存、耗时、并发表现
实战示例:同一接口重构前后的性能对比
以「订单金额计算」接口 GET /api/orders/{id}/amount 为例,重构前该接口在 OrderService 中内联计算,重构后改为调用 OrderAmountCalculator 并复用缓存。测试环境与压测参数如下:
- 测试环境:4 核 8G 容器,JDK 17,Spring Boot 3.2,MySQL 8.0(同机房),关闭日志 debug 级别
- 并发数:50 / 200 两档,wrk 每档持续压测 60 秒
- 样本量:每档取 3 轮结果的中位数,预热 30 秒后开始采集
| 指标 | 重构前 | 重构后 | 变化 |
|---|---|---|---|
| 平均响应耗时(50 并发) | 42.6 ms | 31.8 ms | ↓ 25.4% |
| P99 响应耗时(50 并发) | 118 ms | 76 ms | ↓ 35.6% |
| 平均响应耗时(200 并发) | 156.3 ms | 89.4 ms | ↓ 42.8% |
| P99 响应耗时(200 并发) | 512 ms | 231 ms | ↓ 54.9% |
| 吞吐量 QPS(200 并发) | 1,180 | 2,060 | ↑ 74.6% |
| 堆内存峰值(200 并发) | 1.42 GB | 1.05 GB | ↓ 26.1% |
| Full GC 次数(60 秒内) | 7 | 2 | ↓ 71.4% |
采集命令示例(wrk 压测 + JVM 内存观测):
bash
# 1. 预热 30 秒,避免 JIT 未编译完成导致数据失真
wrk -t4 -c50 -d30s --latency http://localhost:8080/api/orders/1/amount
# 2. 正式采集:50 并发,持续 60 秒,输出延迟分布
wrk -t4 -c50 -d60s --latency http://localhost:8080/api/orders/1/amount
# 3. 200 并发再跑一轮
wrk -t8 -c200 -d60s --latency http://localhost:8080/api/orders/1/amount
# 4. 压测期间另开终端采样堆内存与 GC
jstat -gcutil $(jps | grep OrderApplication | awk '{print $1}') 1000 60
text
Running 1m test @ http://localhost:8080/api/orders/1/amount
8 threads and 200 connections
Latency 89.42ms 31.27ms 231.00ms 88.15%
Req/Sec 2.61k 210.34 3.12k 72.35%
Socket errors: connect 0, read 0, write 0, timeout 0
Requests/sec: 2060.37
Transfer/sec: 412.07KB
# jstat 采样片段(重构后)
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 91.20 68.43 42.17 94.31 89.02 36 0.412 2 0.186 0.598
若需要更细粒度的单方法级基准,可用 JMH 对 OrderAmountCalculator.calc() 做微基准,避免 HTTP 与序列化开销干扰:
java
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(2)
@State(Scope.Thread)
public class OrderAmountBenchmark {
private Order order;
@Setup
public void setUp() {
order = new Order(List.of(new Item(new BigDecimal("19.90"), 3)), null);
}
@Benchmark
public BigDecimal calc() {
return OrderAmountCalculator.calc(order);
}
}
text
$ java -jar benchmarks.jar OrderAmountBenchmark
Benchmark Mode Cnt Score Error Units
OrderAmountBenchmark.calc avgt 10 0.412 ± 0.021 us/op
# 说明:单次计算约 0.4 微秒,说明接口耗时主要来自 I/O 与序列化,
# 重构带来的收益集中在减少重复计算与对象分配,而非算法本身。
需要强调的是,性能数据必须同环境、同参数、多轮取中位数才有可比性;单次压测的波动往往大于重构本身的收益,切忌用一次结果下结论。
6. 常见陷阱与应对
前面几节讲的是「怎么做对」,这一节反过来梳理实践中最容易踩的坑。下表把每个陷阱拆成「典型表现 → 应对动作 → 验证方式」三段,并给出可直接复用的 Claude Code 提示词或 Git 命令,方便对照排查。
| 陷阱 | 典型表现 | 应对动作 | 验证方式 |
|---|---|---|---|
| 一次改动过多 | Claude Code 一次返回十几个文件的 diff,抽取、重命名、格式化混在一起,review 无从下手 | 把任务拆成单点改造,提示词里显式限定范围:> 只修改 {文件路径} 中的 {方法名},不要动其他文件,不要顺带格式化或重命名。改完列出改动文件清单。 |
git diff --stat 确认改动文件数与预期一致;git show --stat HEAD 检查单次提交是否只涉及一个关注点 |
| 上下文不足导致误判 | Claude Code 把「看起来重复」的两段逻辑合并,却漏掉了其中一段的特殊分支;或凭空假设了不存在的工具类 | 先让它只读分析并复述理解:> 阅读 {文件路径},先不要修改任何代码。用中文说明该方法的职责、分支、边界条件与调用方,并指出你不确定的地方。 确认无误后再下达改造指令 |
对照它输出的分支说明与源码逐条核对;改造后跑该模块全量测试,而非只跑新增用例 |
| 忽略测试 | 直接让 Claude Code 改代码,改完只做「能编译」的验证,回归问题在集成阶段才暴露 | 先补特征测试再重构:> 为 {文件路径} 中的 {方法名} 生成特征测试,断言值以当前实际输出为准,覆盖 null、空集合与临界阈值,先不要修改业务代码。 |
mvn -q test -Dtest={测试类名} 全绿后再动手;每批次结束跑一次全量 mvn -q test |
| 缺乏人工审查 | 把 AI 生成的 diff 直接合并,关键模块的语义变更无人确认 | 建立 review 门禁,并要求 AI 先产出改动说明:> 读取 commit {commit_id} 的 diff,用中文总结修改了哪些文件、每个文件改了什么、可能影响哪些调用方,并列出三处最需要人工确认的风险点。 |
PR 必须至少一人 approve;用 git log --oneline baseline/{批次名}..HEAD 逐条核对提交是否都有对应 review 记录 |
| AI 生成的测试断言写成了理想行为而非现状 | 测试全绿,但断言值是按「业务上应该怎样」写的,重构后行为变了测试依然通过,基线形同虚设 | 提示词里强制锁定现状:> 生成特征测试时,断言值必须来自当前代码的实际输出,不要按你认为正确的业务逻辑写;若发现现有行为不合理,用注释标注「已知缺陷」但仍按现状断言。 |
在重构前的代码上跑一遍测试,必须全绿;再故意改一处业务逻辑,确认对应用例会失败(变异自检) |
| 跨批次改动未及时合并导致 diff 冲突 | 多个批次并行推进,各自基于旧基线修改同一批文件,合并时冲突成片,回滚也难以定位归属 | 每个批次独立分支、小步提交并及时合并:git switch -c refactor/{批次名} baseline/{批次名},完成后 git rebase main 再合并;提示词中要求 > 改动前先说明本次会触碰哪些文件,若与 {其他批次} 重叠请先提示我。 |
合并前 git diff main...HEAD --stat 检查重叠文件;CI 上跑全量测试与 SonarQube 质量门禁,冲突解决后再复跑一次 |
几点补充:
- 陷阱往往成对出现:改动过多常伴随审查缺失,测试断言写错又常源于上下文不足。排查时先看「典型表现」,再顺着「验证方式」定位是哪一环断了。
- 把验证方式固化进流程:上表的验证命令建议直接写进 CI 或提交模板,靠人记的检查项最终都会漏。
- 复盘比预防更有效:每批次结束后花十分钟记录踩过的坑,补进 4.5 节的提示词模板,下一批次就能少踩一次。
7. 总结
用 Claude Code 重构遗留系统,核心在于「人机协作」:让 AI 承担分析、编码、测试生成等重复性工作,而人类负责目标设定、策略决策与关键审查。只要遵循「评估 → 规划 → 小步执行 → 持续验证」的路径,就能在控制风险的前提下,让遗留系统逐步走向现代化。
8. 参考资料
- 《重构:改善既有代码的设计》(第 2 版) ------Martin Fowler 的经典著作,系统讲解抽取方法、消除重复代码等重构手法,对应本文「4.2 小步改造与即时验证」与「4.3 善用 Claude Code 的辅助能力」中的抽取实践。
- StranglerFigApplication(绞杀者模式) ------Martin Fowler 关于渐进式替换遗留系统的经典文章,对应本文「3. 制定重构策略:分层推进,降低风险」中按批次推进的整体思路。
- Claude Code 官方文档 ------Claude Code 的安装、权限、上下文管理与常用命令说明,对应本文「4.1 让 Claude Code 理解现状」与「4.5 常用提示词模板」。
- JUnit 5 用户指南 ------
@Nested、@DisplayName、assertThrows等 API 的权威说明,对应本文「5.1 自动化测试兜底」中的特征测试代码。 - JMH 官方示例(OpenJDK jmh-samples) ------微基准的
@Benchmark、@Warmup、@Fork等注解用法示例,对应本文「5.3 性能与稳定性验证」中的 JMH 基准代码。 - SonarQube 官方文档 ------ 质量门禁、圈复杂度与坏味道规则的配置说明,对应本文「工具链与替代方案对比」与「6. 常见陷阱与应对」中的 CI 门禁部分。
- jscpd(Copy/Paste Detector)文档 ------ 重复代码检测工具的阈值配置与报告格式,对应本文「4.3 善用 Claude Code 的辅助能力」中按 jscpd 思路扫描重复片段的示例。
- wrk 压测工具 ------ HTTP 压测命令行参数与延迟统计说明,对应本文「5.3 性能与稳定性验证」中的采集命令示例。
- Pro Git:Git 基础与撤销操作 ------
revert、reset、分支与标签的权威说明,对应本文「4.2 改造失败时怎么回滚」中的回滚命令。