代码重构实战:从"能跑"到"好改"的工程方法
重构不是"觉得代码烂就重写"——而是有步骤、有验证、有节奏地改善代码结构。本文从重构的时机选择、安全重构的步骤、常见重构手法、以及如何让重构成为日常习惯四个层面展开——适合正在维护中大型项目的后端和前端的开发者。
先说结论:重构不是”重写”,是”改善”
很多开发者看到烂代码的第一反应是”重写”,但重写的风险远高于重构——重写过程中,你可能会丢失对边界情况的处理、引入新 Bug、而且业务方不会等你”重写完了再上线”。
重构的核心原则是:每次只改一点点,改完确保还能跑。
1. 重构的时机
值得重构的信号
| 信号 | 表现 | 解决方案 |
|---|---|---|
| 重复代码 | 同一逻辑出现在多处 | 抽取函数或类 |
| 过长函数 | 一个函数超过 50 行 | 拆分为多个小函数 |
| 过大类 | 一个类超过 500 行 | 拆分职责 |
| 过度耦合 | 改一处要改多处 | 引入接口或依赖注入 |
| 命名混乱 | 变量名不能表达意图 | 重命名 |
不值得重构的场景
- 代码即将被替换(下个月就要下线)
- 代码几乎不被修改(稳定运行、没人动)
- 没有测试覆盖且无法补测试(遗留系统,即将报废)
2. 安全重构的步骤
2.1 补测试
// 特征测试:锁定当前行为
describe('formatPrice', () => {
it('formats price correctly', () => {
// 不管当前输出是什么,先记录下来
expect(formatPrice(1000)).toBe('¥1,000.00');
expect(formatPrice(2500.5)).toBe('¥2,500.50');
});
});
2.2 小步重构
每次只做一种重构操作,做完就跑测试:
// 第一步:重命名(rename)
// 改前
function calc(a: number, b: number) { ... }
// 改后
function calculateDiscount(originalPrice: number, level: string) { ... }
// 第二步:提取函数(extract function)
// 改前
const total = items.reduce((sum, i) => sum + i.price * i.qty, 0);
const tax = total * 0.1;
const grandTotal = total + tax;
// 改后
function calculateSubtotal(items: Item[]): number { ... }
function calculateTax(subtotal: number): number { ... }
function calculateTotal(items: Item[]): number { ... }
2.3 小步提交
每个小步骤完成后,提交一次代码。提交信息写清楚”重构了什么、为什么重构”。
git commit -m "refactor: 抽取 calculateDiscount 函数,替换内联计算"
3. 常见重构手法
| 手法 | 适用场景 | 说明 |
|---|---|---|
| 重命名(Rename) | 命名不能表达意图 | IDE 自动重构,安全 |
| 提取函数(Extract) | 函数过长或逻辑重复 | 将一段逻辑提取为独立函数 |
| 提取变量(Extract Variable) | 表达式难以理解 | 用有意义的变量名解释表达式 |
| 拆分条件(Decompose) | 条件表达式过长 | 将条件拆分为多个函数 |
| 引入参数(Introduce Parameter) | 硬编码的值 | 将硬编码值改为参数 |
| 移动方法(Move Method) | 方法放在错误的类中 | 将方法移到更合适的类 |
4. 让重构成为习惯
童子军规则
“离开时让营地比来时更干净”——每次修改代码时,顺手改善一下你碰到的代码。不需要专门安排重构时间,每次改 Bug 或加功能时顺手做一点。
重构时间预算
| 场景 | 建议时间 |
|---|---|
| 修 Bug 时 | 顺手重构 5-10 分钟 |
| 加小功能时 | 先重构受影响的代码 15-30 分钟 |
| 加大功能时 | 规划 1-2 天专门重构 |
| 代码 Review 时 | 如果发现坏味道,记录下来下次重构 |
总结
| 原则 | 说明 |
|---|---|
| 先补测试 | 没有测试的重构是赌博 |
| 小步前进 | 每次只做一种重构操作 |
| 频繁提交 | 每步一个 commit,方便回滚 |
| 融入日常 | 不专门安排”重构月” |
| 童子军规则 | 每次修改都让代码好一点点 |
重构不是”等代码烂到不行了再大修”,而是”每次经过都把它修好一点点”。 一个每天改进 1% 的代码库,半年后和一个每天堆积 1% 技术债务的代码库,差距是巨大的。
需要代码重构或技术咨询?联系我们,说清你的项目规模与痛点,24 小时内回可行性。
相关阅读
- 技术方案评审怎么做:选型、架构评审与可行性报告的方法 —— 重构前需要先做技术评审
- Code Review 实战:从 Checklist 到工程文化的落地方法 —— 代码质量保障的上下游实践
常见问题
重构和重写的区别是什么?
重构是在不改变外部行为的前提下改善内部结构——功能不变,代码变好。重写是推倒重来——功能可能变,代码全部重写。重构的每一步都是可验证的(测试通过),风险可控;重写的风险高得多,尤其在缺乏测试覆盖的情况下。建议:除非代码已经完全无法维护,否则优先选择重构。
什么时候应该重构?
三种情况值得重构:① 每次添加新功能之前——先重构受影响的代码,让新功能更容易加入("童子军规则":离开时让营地比来时更干净);② 修复 Bug 时——如果 Bug 是因为代码难以理解导致的,先重构再修;③ 代码出现"坏味道"时——重复代码、过长函数、过大类、过度耦合。不要专门安排"重构月",重构应该融入日常开发。
重构没有测试怎么办?
先补测试,再重构。如果代码完全没有测试,重构就是在赌博。做法:① 先用 Characterization Test(特征测试)锁定当前行为——写一个测试调用函数,断言"当前输出是什么",不管它是对是错;② 然后重构,确保重构后的输出和重构前一样;③ 如果发现 Bug,先修 Bug 再重构。
大重构怎么拆解?
大重构必须拆成小步骤。每个小步骤完成后,代码都是可运行、可测试的。推荐的拆解方式:① 按模块拆分——一次只重构一个模块,完成后验证再继续下一个;② 按层次拆分——先重构测试层,再重构接口层,最后重构实现层;③ 使用"绞杀者模式"——为新功能写新代码,逐步将调用方迁移到新代码,最后删掉旧代码。