用 Claude Code 安全现代化遗留代码
用 Claude Code 为遗留代码补测试、分阶段重构和迁移 TypeScript,包含陷阱、示例和验证结果。
遗留代码现代化不能从大改开始
遗留代码不只是“年代久远的代码”。更准确地说,它是团队不敢轻易修改的代码:测试覆盖不足,业务规则散落在条件分支里,文档已经过期,原作者也不一定还在。Claude Code 在这种场景很有价值,但它不应该被当成一键重写工具。
安全的顺序是先调查,再用测试固定现有行为,然后做很小的重构。这里的测试更接近 characterization test,也就是先记录“系统现在到底怎么表现”,而不是立刻讨论“理想实现应该是什么”。harness 可以理解为“给代理搭的脚手架”:测试、权限、命令、项目规则和审核清单都属于脚手架。
官方 Claude Code common workflows 把代码探索、重构、测试、PR 和 worktree 并行工作放在同一个工作流里。做遗留代码现代化时,这个思路非常重要:Claude Code 提高执行速度,人类仍然负责风险边界和验收。
三个最常见的实战场景
不要因为某段代码“看起来丑”就优先重构。先从能降低业务风险的地方开始。
| 场景 | 目标 | Claude Code 适合做什么 | 人类必须确认什么 |
|---|---|---|---|
| 订单、账单、支付逻辑 | 避免金额和客户状态错误 | 梳理现有行为、补测试、找边界值 | 税率、折扣、舍入、合规规则 |
| JavaScript 迁移到 TypeScript | 降低后续修改风险 | 生成类型、逐步减少 any、修复类型错误 | 对外 API 兼容性 |
| 回调地狱或巨型函数拆分 | 提高可读性和可维护性 | 拆分职责、建议命名、解释差异 | 错误处理、副作用、重试逻辑 |
我在 ClaudeCodeLab 的内容脚本、结账流程和旧数据转换代码里也用同样的方法。关键不是让 Claude Code 写得越快越好,而是让每个变更小到可以审核,并且每个风险点都有测试或人工确认记录。
flowchart LR
A[调查] --> B[用测试固定行为]
B --> C[小步重构]
C --> D[增加类型并更新依赖]
D --> E[人工审核风险]
E --> B
先做只读审计
第一次提示词要明确禁止编辑。你需要的是系统地图,而不是基于不完整上下文的补丁。
请阅读 @src/legacy 和 @test。
现在不要修改任何文件。
请按下面格式输出审计结果:
1. 主要文件和职责
2. 外部 I/O、数据库、API、文件写入和副作用
3. 必须保持兼容的现有行为
4. 缺失测试和高风险分支
5. 最安全的小步修改顺序
如果规则不清楚,请写“需要人工确认”,不要猜测。
官方 How Claude Code works 说明 Claude Code 可以读取文件、运行命令并编辑代码。能力越强,越需要把第一阶段限制在理解和计划上。尤其是旧系统,一次看似合理的“优化”可能改变外部依赖已习惯的返回值。
可复制运行的最小示例
下面的例子是一个缩小版订单处理器。真实项目可能还会连接数据库、消息队列或外部支付 API,但步骤相同。先创建项目。
mkdir legacy-modernization-demo
cd legacy-modernization-demo
npm init -y
npm install -D vitest typescript @types/node
npm pkg set type="module"
npm pkg set scripts.test="vitest run"
npm pkg set scripts.typecheck="tsc --noEmit"
mkdir -p src/legacy test
旧实现把验证、计算和返回值组装放在同一个文件里。这不是夸张的坏代码,而是很多团队真正遇到的“能跑,但没人想碰”的代码。
// src/legacy/orderProcessor.js
export function processOrder(order) {
if (!order || !Array.isArray(order.items) || order.items.length === 0) {
return { status: "error", message: "items is required" };
}
const subtotal = order.items.reduce((sum, item) => {
return sum + item.price * item.qty;
}, 0);
const discount = order.customer?.type === "vip" ? subtotal * 0.1 : 0;
return {
status: "confirmed",
total: subtotal - discount,
items: order.items,
discount
};
}
先写测试,锁定现有行为。这里测试的是“不能被破坏的行为”,不是马上设计更完美的业务模型。
// test/orderProcessor.test.ts
import { describe, expect, it } from "vitest";
import { processOrder } from "../src/legacy/orderProcessor.js";
describe("processOrder legacy behavior", () => {
it("calculates total for a regular customer", () => {
const result = processOrder({
items: [
{ id: "A1", qty: 2, price: 1000 },
{ id: "B2", qty: 1, price: 500 }
],
customer: { id: "C1", type: "regular" }
});
expect(result).toMatchObject({
status: "confirmed",
total: 2500,
discount: 0
});
});
it("applies a 10 percent VIP discount", () => {
const result = processOrder({
items: [{ id: "A1", qty: 1, price: 10000 }],
customer: { id: "C2", type: "vip" }
});
expect(result.status).toBe("confirmed");
expect(result.total).toBe(9000);
expect(result.discount).toBe(1000);
});
it("returns an error when items are empty", () => {
const result = processOrder({
items: [],
customer: { id: "C3", type: "regular" }
});
expect(result.status).toBe("error");
expect(result.message).toContain("items");
});
});
运行 npm test。通过后,再让 Claude Code 做改动。
请阅读 @src/legacy/orderProcessor.js 和 @test/orderProcessor.test.ts。
在保持现有测试通过的前提下迁移到 TypeScript。
约束:
- 保留公开函数名 processOrder
- 保持 status、total、discount、message 的兼容行为
- 先加类型定义,再拆分职责
- 修改后运行 npm test 和 npm run typecheck
- 说明每个差异保留了什么兼容性
TypeScript 化后的结构
改造后的结构把类型、验证、计算和编排分开。这样做不是为了抽象而抽象,而是为了让金额相关逻辑更容易审核。
// src/orderTypes.ts
export type CustomerType = "regular" | "vip";
export type OrderItem = {
id: string;
qty: number;
price: number;
};
export type OrderInput = {
items: OrderItem[];
customer: {
id: string;
type: CustomerType;
};
};
export type OrderResult =
| {
status: "confirmed";
total: number;
items: OrderItem[];
discount: number;
}
| {
status: "error";
message: string;
};
// src/validators.ts
import type { OrderInput } from "./orderTypes";
export function validateOrder(order: OrderInput | null | undefined): string | null {
if (!order || !Array.isArray(order.items) || order.items.length === 0) {
return "items is required";
}
return null;
}
// src/calculators.ts
import type { CustomerType, OrderItem } from "./orderTypes";
export function calculateSubtotal(items: OrderItem[]): number {
return items.reduce((sum, item) => sum + item.price * item.qty, 0);
}
export function calculateDiscount(subtotal: number, customerType: CustomerType): number {
return customerType === "vip" ? subtotal * 0.1 : 0;
}
// src/orderProcessor.ts
import { calculateDiscount, calculateSubtotal } from "./calculators";
import type { OrderInput, OrderResult } from "./orderTypes";
import { validateOrder } from "./validators";
export function processOrder(order: OrderInput): OrderResult {
const validationMessage = validateOrder(order);
if (validationMessage) {
return { status: "error", message: validationMessage };
}
const subtotal = calculateSubtotal(order.items);
const discount = calculateDiscount(subtotal, order.customer.type);
return {
status: "confirmed",
total: subtotal - discount,
items: order.items,
discount
};
}
把测试 import 改成 ../src/orderProcessor,再运行 npm test 和 npm run typecheck。如果差异保持这种规模,审核者还能看清楚发生了什么。若同一个 PR 同时做目录移动、依赖升级、格式化、命名变化和业务规则变化,风险会明显上升。
依赖升级要分开处理
另一个常见失败是把重构和依赖大版本升级混在一起。测试失败时,你无法判断是类型迁移、API 破坏性变更、构建工具变更,还是业务逻辑变了。
可以先让 Claude Code 只做清单。
请阅读 package.json 和 lockfile。
现在不要更新任何依赖。
请输出表格:
- package
- 当前版本
- 推荐目标版本
- 是否是 major upgrade
- 官方迁移指南 URL
- 可能影响的文件
- 更新前应该补的测试
涉及删除文件、迁移脚本、部署命令时,权限要保守。官方 Claude Code permissions 文档值得在团队导入前阅读。AI 执行得快不是问题,问题是它是否绕过了本来需要人工确认的风险点。
具体陷阱
第一个陷阱是没有测试就重构。代码变清爽了,但金额舍入、折扣条件、错误文案变了,就是回归问题。
第二个陷阱是把 Claude Code 的建议当成业务规则。更符合 TypeScript 风格的返回值,不代表旧客户端能接受。
第三个陷阱是巨大 PR。类型迁移、逻辑拆分、依赖更新、文件移动和格式化最好拆开提交。
第四个陷阱是过度改善错误处理。旧系统里奇怪的 null、字符串或 HTTP 状态码,可能已经是外部契约的一部分。
第五个陷阱是不写变更说明。让 Claude Code 在 PR 里写兼容性说明、手动验证步骤和回滚方法,可以减少之后的维护成本。
审核流程和下一步
这篇文章可以和重构自动化指南、Claude Code 与 TDD、文档自动生成一起使用。团队导入时,也建议把禁止修改区域、测试命令、领域术语和审核规则写进 CLAUDE.md 最佳实践。
ClaudeCodeLab 提供 Claude Code 培训、CLAUDE.md 模板和遗留系统现代化咨询。目标不是用 AI 一次性重写,而是建立可重复的工作流,让测试、权限和人工审核一起降低风险。
实际验证结果
我按本文流程创建了 legacy-modernization-demo:先写旧 JavaScript 订单处理器,再用 Vitest 固定 3 个行为,然后迁移到 TypeScript 并重新跑测试和类型检查。最有效的一点是把“只读审计”和“允许编辑”分成两次请求。这样 Claude Code 生成的差异更小,金额计算、VIP 折扣和空订单错误的兼容性也更容易确认。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
从Obsidian到CLAUDE.md的Claude Code流程:不再反复解释上下文
把 Obsidian 工作笔记整理成 CLAUDE.md 运行说明,让 Claude Code 每次都带着正确上下文开始。
Claude Code 收入 CTA 路由:从文章分流到 PDF、Gumroad 与咨询
用 Claude Code 按读者意图把文章流量分到免费 PDF、Gumroad 教材或咨询入口。
Claude Code 团队交接规则: 把审查证据、权限、回滚和收入路径一起交付
面向团队的 Claude Code 交接格式: 证据、权限、回滚、免费 PDF、Gumroad 与咨询路径都要可审查。