学习目标:掌握使用Claude Code识别和修复五种常见代码坏味道的完整工作流,学会从"发现臭味"到"设计模式升级"的渐进式重构方法,最终能在真实项目中用Claude Code安全地完成跨文件重构。
适用人群:有1年以上开发经验、熟悉TypeScript/Python、正在使用或准备使用Claude Code提升代码质量的开发者。
| 条件 | 说明 |
|---|---|
| Claude Code CLI | 已安装并完成认证(claude --version ≥ 1.0) |
| 项目语言 | 本教程示例基于TypeScript,方法通用 |
| Git | 重构前必须能创建检查点 |
| 测试框架 | Jest/Vitest/pytest,用于行为保护 |
| CLAUDE.md | 项目根目录已有基本配置 |
传统重构有三个痛点:识别慢(人眼扫描数百行找坏味道)、修改险(改一处动全局)、验证难(手动跑测试覆盖不全)。Claude Code的独特优势在于它能同时持有整个模块的上下文——100K+ token的上下文窗口意味着它可以"看清"跨文件的依赖关系,而不仅仅是当前文件。
根据Ralphable对12个生产项目的统计,使用Claude Code进行重构的典型收益:
关键数据点:使用Claude Code重构的团队报告生产力提升55-80%,一个210行、圈复杂度16的Python函数被重构为30行以下、复杂度3-6的多个函数。
在执行任何重构之前,必须建立安全网。这是非可选步骤。
# 重构前创建检查点
git add -A && git commit -m "checkpoint: before refactoring session"
# 运行现有测试确认基线
pnpm test -- --coverage
# 如果没有测试,让Claude Code先生成特征测试
claude "为 src/utils/permissions.ts 编写特征测试(characterization tests),
覆盖所有分支和边界情况。不要修改源代码,只记录当前行为。"
在项目根目录的CLAUDE.md中添加重构相关规则:
## 重构规则
- 重构前必须确认所有测试通过
- 每次重构只做一件事,不要混合多种重构手法
- 保持行为不变:重构后所有现有测试必须仍然通过
- 优先使用项目已有的工具库(如Zod用于验证)
- 不要引入新的依赖,除非必要且经过讨论
- 重构完成后运行完整测试套件验证
flowchart TD
A[发现代码坏味道] --> B{有测试保护?}
B -->|有| D[启动Claude Code重构]
B -->|没有| C[生成特征测试]
C --> C1[claude: 编写characterization tests]
C1 --> C2[运行测试确认基线通过]
C2 --> D
D --> E[选择重构手法]
E --> F1[手法1: 提取组件/函数]
E --> F2[手法2: 简化条件逻辑]
E --> F3[手法3: 消除重复代码]
E --> F4[手法4: 类型安全加固]
E --> F5[手法5: 设计模式升级]
F1 --> G[运行测试验证]
F2 --> G
F3 --> G
F4 --> G
F5 --> G
G --> H{测试全部通过?}
H -->|是| I[Git提交重构结果]
H -->|否| J[回滚到检查点]
J --> K[分析失败原因]
K --> D
I --> L[下一种坏味道]
L --> A
style A fill:#ff6b6b,color:#fff
style D fill:#4ecdc4,color:#fff
style G fill:#45b7d1,color:#fff
style I fill:#96ceb4,color:#fff
style J fill:#ff6b6b,color:#fff
一个函数超过100行、做了超过一件事、包含多个层级的UI或逻辑,就是典型的"上帝函数"(God Function)。
提取 [文件路径] 中的 [功能区块] 为独立组件 [组件名]。
要求:
1. Props只传递真正需要的数据,不要prop drilling
2. 保持现有行为完全一致
3. 状态归属于最合适的组件
4. 重构后运行测试验证
重构前(pages/dashboard.tsx,400行):
export default function Dashboard() {
const [notifications, setNotifications] = useState<Notification[]>([]);
const [filter, setFilter] = useState<"all" | "unread">("all");
// ...200行其他Dashboard逻辑...
const filtered = notifications.filter(n =>
filter === "all" ? true : !n.readAt
);
return (
<div className="grid grid-cols-12 gap-6">
{/* 200行业务内容 */}
<aside className="col-span-3">
{/* 80行通知面板逻辑 */}
<div className="flex items-center justify-between mb-4">
<h2>Notifications</h2>
<select value={filter} onChange={e =>
setFilter(e.target.value as "all" | "unread")
}>
<option value="all">All</option>
<option value="unread">Unread</option>
</select>
</div>
<ul>
{filtered.map(n => (
<li key={n.id} className={n.readAt ? "opacity-60" : ""}>
<p>{n.message}</p>
<time>{n.createdAt}</time>
</li>
))}
</ul>
</aside>
</div>
);
}
执行重构:
claude "提取 pages/dashboard.tsx 中的通知面板为独立组件 NotificationPanel。
Props只传notifications数组。过滤状态留在组件内部。
保持行为一致,重构后运行 pnpm test 验证。"
重构后——components/NotificationPanel.tsx:
interface NotificationPanelProps {
notifications: Notification[];
}
export function NotificationPanel({ notifications }: NotificationPanelProps) {
const [filter, setFilter] = useState<"all" | "unread">("all");
const filtered = notifications.filter(n =>
filter === "all" ? true : !n.readAt
);
return (
<aside className="col-span-3">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">Notifications</h2>
<select value={filter} onChange={e =>
setFilter(e.target.value as "all" | "unread")
}>
<option value="all">All</option>
<option value="unread">Unread</option>
</select>
</div>
<ul>
{filtered.map(n => (
<li key={n.id} className={n.readAt ? "opacity-60" : ""}>
<p>{n.message}</p>
<time>{n.createdAt}</time>
</li>
))}
</ul>
</aside>
);
}
关键决策:Claude Code正确判断了filter状态属于NotificationPanel而非Dashboard——因为过滤只影响通知面板。这种"所有权边界"的判断是手动重构容易遗漏的。
| 问题 | 解决方案 |
|---|---|
| 提取后类型报错 | 确保新文件有正确的import,类型已导出 |
| 测试失败 | 检查是否有测试直接引用了内部实现细节 |
| Props过多 | 检查是否需要引入Context而非逐层传递 |
函数中有超过3层嵌套的if/else,形成"箭头形"代码(向右缩进越来越深),阅读时需要同时记住多个条件分支。
简化 [文件/函数] 中的条件逻辑。
要求:
1. 用early return(卫语句)替代嵌套if/else
2. 提取复杂条件为有意义的命名变量
3. 保持每个分支的行为不变
4. 如果某个分支看起来是bug,加注释说明但不要修改行为
重构前(圈复杂度=6,最大嵌套=4层):
function canUserEdit(user: User, document: Document): boolean {
if (user.role === "admin") {
return true;
} else {
if (document.ownerId === user.id) {
if (document.status !== "archived") {
return true;
} else {
if (user.permissions.includes("edit_archived")) {
return true;
} else {
return false;
}
}
} else {
if (document.collaborators.includes(user.id)) {
if (document.status === "draft" || document.status === "review") {
return true;
} else {
return false;
}
} else {
return false;
}
}
}
}
执行重构:
claude "简化 src/utils/permissions.ts 中 canUserEdit 函数的条件逻辑。
用early return和卫语句替代嵌套。提取条件为语义化变量。
运行测试确认行为不变。"
重构后(圈复杂度=4,最大嵌套=1层):
function canUserEdit(user: User, document: Document): boolean {
if (user.role === "admin") return true;
const isOwner = document.ownerId === user.id;
const isCollaborator = document.collaborators.includes(user.id);
const isArchived = document.status === "archived";
const editableStatuses = ["draft", "review"];
if (isOwner && !isArchived) return true;
if (isOwner && isArchived) return user.permissions.includes("edit_archived");
if (isCollaborator) return editableStatuses.includes(document.status);
return false;
}
改进量化:
| 指标 | 重构前 | 重构后 |
|---|---|---|
| 代码行数 | 27行 | 12行 |
| 最大嵌套深度 | 4层 | 1层 |
| 圈复杂度 | 6 | 4 |
| 可读性评分 | 差 | 优 |
两个或多个文件中存在几乎相同的逻辑块,通常是复制粘贴后做了微调。修改时需要同步更新多处,容易遗漏。
搜索 [目录] 中所有重复的验证逻辑。
提取共享逻辑到 [目标路径] 的工具函数/Schema中。
用Zod schema(如果项目已有)替代手动验证。
更新所有调用点,运行测试。
重构前——两个路由文件中几乎相同的验证:
// routes/create-project.ts
const body = await req.json();
if (!body.name || typeof body.name !== "string" || body.name.length > 100) {
return Response.json({ error: "Invalid name" }, { status: 400 });
}
if (!body.teamId || typeof body.teamId !== "string") {
return Response.json({ error: "Invalid team" }, { status: 400 });
}
// routes/update-project.ts(几乎一样的验证代码)
const body = await req.json();
if (!body.name || typeof body.name !== "string" || body.name.length > 100) {
return Response.json({ error: "Invalid name" }, { status: 400 });
}
if (!body.teamId || typeof body.teamId !== "string") {
return Response.json({ error: "Invalid team" }, { status: 400 });
}
执行重构:
claude "找出 routes/ 目录中所有重复的请求验证逻辑。
提取为 lib/validation/project.ts 中的Zod schema。
更新所有路由文件使用schema验证。运行 pnpm test 验证。"
重构后:
// lib/validation/project.ts
import { z } from "zod";
export const projectSchema = z.object({
name: z.string().min(1).max(100),
teamId: z.string().min(1),
});
export type ProjectInput = z.infer<typeof projectSchema>;
// routes/create-project.ts
import { projectSchema } from "@/lib/validation/project";
const parsed = projectSchema.safeParse(await req.json());
if (!parsed.success) {
return Response.json(
{ error: parsed.error.flatten() },
{ status: 400 }
);
}
Claude Code的智能升级:它不只是"搬代码",而是自动将手动验证升级为Zod schema——更安全、更易维护,还自动生成了类型定义。新增字段只需改一个文件。
any类型代码中存在大量any类型标注,或完全没有类型标注的函数。这是TypeScript项目中最常见的技术债。
为 [文件/模块] 添加严格的TypeScript类型。
要求:
1. 替换所有any为正确的接口定义
2. 从运行时用法推断类型(如果没有现成定义)
3. 不要使用as类型断言——修复底层类型
4. 为泛型参数添加约束
重构前:
export async function fetchData(endpoint: string, options?: any): Promise<any> {
const res = await fetch(`/api/${endpoint}`, options);
const data: any = await res.json();
return data;
}
// 30+组件中的用法
const users: any = await fetchData("users");
const projects: any = await fetchData("projects", {
method: "POST",
body: JSON.stringify({ name: "New Project" })
});
执行重构:
claude "为 lib/api.ts 添加严格TypeScript类型。
替换所有any。使用泛型让调用方指定返回类型。
不要用as断言。修复底层类型问题。运行 pnpm test 验证。"
重构后:
interface FetchOptions extends Omit<RequestInit, "body"> {
body?: Record<string, unknown>;
}
export async function fetchData<T>(
endpoint: string,
options?: FetchOptions
): Promise<T> {
const { body, ...rest } = options ?? {};
const res = await fetch(`/api/${endpoint}`, {
...rest,
body: body ? JSON.stringify(body) : undefined,
headers: {
"Content-Type": "application/json",
...rest.headers,
},
});
if (!res.ok) {
throw new ApiError(res.status, await res.text());
}
return res.json() as Promise<T>;
}
// 用法——类型安全
const users = await fetchData<User[]>("users");
const project = await fetchData<Project>("projects", {
method: "POST",
body: { name: "New Project" }
});
改进量化:
any类型使用:从3处降至0处大量switch/case或if/else链条根据类型做不同处理,每次新增类型都要修改已有代码(违反开闭原则)。
将 [函数/模块] 中的switch/case重构为 [策略模式/工厂模式/状态模式]。
要求:
1. 定义统一的接口/抽象类
2. 每种情况实现为独立类
3. 用注册表或工厂方法管理实例
4. 确保新增类型只需添加新类,不修改已有代码
5. 运行测试验证行为不变
重构前(违反开闭原则):
async function sendNotification(type: string, payload: any): Promise<void> {
switch (type) {
case "email":
await emailClient.send({
to: payload.to,
subject: payload.subject,
body: payload.body,
});
break;
case "sms":
await smsClient.send({
phone: payload.phone,
message: payload.message,
});
break;
case "push":
await pushClient.send({
userId: payload.userId,
title: payload.title,
body: payload.body,
});
break;
case "slack":
await slackClient.postMessage({
channel: payload.channel,
text: payload.text,
});
break;
default:
throw new Error(`Unknown notification type: ${type}`);
}
}
执行重构:
claude "将 src/notifications/send.ts 中的sendNotification函数重构为策略模式。
定义Notifier接口,每种通知类型实现为独立类。
用Map注册表替代switch。新增通知类型只需添加新类。
运行测试确认行为不变。"
重构后:
// types.ts
interface Notifier<T = unknown> {
type: string;
send(payload: T): Promise<void>;
}
// notifiers/email.ts
interface EmailPayload {
to: string;
subject: string;
body: string;
}
class EmailNotifier implements Notifier<EmailPayload> {
type = "email";
async send(payload: EmailPayload): Promise<void> {
await emailClient.send(payload);
}
}
// notifiers/sms.ts
interface SmsPayload {
phone: string;
message: string;
}
class SmsNotifier implements Notifier<SmsPayload> {
type = "sms";
async send(payload: SmsPayload): Promise<void> {
await smsClient.send(payload);
}
}
// registry.ts
const notifierRegistry = new Map<string, Notifier>();
export function registerNotifier(notifier: Notifier): void {
notifierRegistry.set(notifier.type, notifier);
}
export async function sendNotification(
type: string,
payload: unknown
): Promise<void> {
const notifier = notifierRegistry.get(type);
if (!notifier) throw new Error(`Unknown notification type: ${type}`);
await notifier.send(payload);
}
// 初始化注册
registerNotifier(new EmailNotifier());
registerNotifier(new SmsNotifier());
registerNotifier(new PushNotifier());
registerNotifier(new SlackNotifier());
开闭原则达成:新增Discord通知只需要创建DiscordNotifier类并注册,无需修改sendNotification函数。
当重构涉及10个以上文件时,需要一套系统化的工作流:
claude "我要将整个项目中的Plan类型重命名为SubscriptionTier,
因为与Stripe的Plan类型冲突。
请先分析影响范围:列出所有需要修改的文件和位置。"
Claude Code会返回类似的分析:
影响范围分析:
- 类型定义:types/subscription.ts(1处)
- 组件引用:14个React组件
- API路由:8个路由处理程序
- 测试文件:12个测试文件
- 配置文件:3个(constants、seed、migration)
- 字符串字面量:5处UI文本
总计:83个文件,预计修改~120处
建议分三阶段执行:
Phase 1: 重命名类型定义和导出
Phase 2: 更新所有import和类型引用
Phase 3: 更新字符串字面量和UI文本(需人工确认)
# Phase 1: 核心类型重命名
claude "执行Phase 1:重命名 types/subscription.ts 中的Plan为SubscriptionTier。
不要修改其他文件,只改类型定义和导出。运行测试。"
# 确认通过后
git add -A && git commit -m "refactor: rename Plan to SubscriptionTier (phase 1)"
# Phase 2: 更新所有引用
claude "执行Phase 2:更新所有14个组件、8个API路由、12个测试文件中的
Plan类型引用为SubscriptionTier。更新import语句。
运行 pnpm test 验证。"
git add -A && git commit -m "refactor: update all references (phase 2)"
# Phase 3: 字符串字面量(需要人工判断)
claude "执行Phase 3:列出所有包含'Plan'字符串的UI文本和字符串字面量。
不要自动修改——输出列表让我决定哪些需要改。"
# 运行完整测试套件
pnpm test
# TypeScript类型检查
pnpm tsc --noEmit
# 代码搜索确认无遗漏
claude "搜索整个项目,确认没有遗漏的Plan引用
(排除node_modules和.lock文件)。只报告类型相关的引用,
不报告注释中的。"
将常用的重构流程封装为自定义命令,避免每次重复输入提示词:
创建.claude/commands/refactor-extract.md:
提取 $ARGUMENTS 中的指定功能区块为独立模块。
执行步骤:
1. 读取目标文件,理解当前结构
2. 识别要提取的代码边界
3. 创建新文件,将代码移入
4. 更新原文件的import和引用
5. 运行 pnpm test 验证行为不变
6. 报告修改的文件列表和测试结果
约束:
- Props/参数只传递必要数据
- 状态归属于最合适的模块
- 不改变任何外部可见行为
使用方式:
claude "/refactor-extract pages/dashboard.tsx 中的通知面板"
每次重构完成后,用以下指标量化改进:
claude "分析以下文件的重构改进,输出量化报告:
- 圈复杂度变化
- 代码行数变化
- 函数/组件数量变化
- 类型安全覆盖率变化
- 重复代码比例变化
对比文件:[重构前文件] 和 [重构后文件]"
典型输出:
重构量化报告:
┌─────────────────┬──────────┬──────────┬──────────┐
│ 指标 │ 重构前 │ 重构后 │ 变化 │
├─────────────────┼──────────┼──────────┼──────────┤
│ 圈复杂度(最大) │ 16 │ 4 │ -75% │
│ 代码行数 │ 210 │ 85 │ -60% │
│ any类型数量 │ 12 │ 0 │ -100% │
│ 重复代码块 │ 8 │ 0 │ -100% │
│ 函数平均长度 │ 45行 │ 12行 │ -73% │
│ 测试覆盖率 │ 34% │ 89% │ +162% │
└─────────────────┴──────────┴──────────┴──────────┘
# 错误做法:一个提示词要求重构整个模块
claude "重构整个 auth/ 模块,让它更干净" # ❌ 结果不可控
# 正确做法:一次只做一种重构
claude "只用'提取函数'手法重构 auth/validate.ts,
不要做其他修改" # ✅ 行为可预测
# 错误做法:直接改代码
claude "把这个switch改成策略模式" # ❌ 没有安全网
# 正确做法:先确保测试覆盖
claude "先为 sendNotification 写特征测试覆盖所有分支。
测试通过后,再将switch重构为策略模式。" # ✅ 安全重构
Claude Code的重构结果应该像初级开发者的PR一样审查:
null/undefined检查| 维度 | 说明 |
|---|---|
| 适合训练营类型 | 企业内训、高级开发者训练营、代码质量提升工作坊 |
| 学员基础要求 | 1年以上TypeScript/Python经验,了解基本设计模式 |
| 预计教学时长 | 4-6小时(含实操练习) |
| 实操占比 | 70%(每种手法配一个现场练习) |
| 课后作业 | 选择自己项目中的真实代码坏味道,用五步法重构并提交量化报告 |
| 难度评级 | 中高级 |
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
