重构(Refactoring)是 Martin Fowler 1999 年定义的:"在不改变软件外部行为的前提下,改善其内部结构"。日常开发里,重构和写新功能同样重要 —— 没有持续重构,代码债务会让项目越走越慢。这篇文章把重构的核心手法、识别坏味道、安全重构的工程实践一次讲透。
什么时候重构
Martin Fowler 的"露营规则":离开时让代码比来时更干净一点。具体时机:
- 写新功能时,顺手清理碰到的乱代码。
- 修 bug 时,先重构出可测试的结构,再修。
- Code Review 发现重复代码 / 坏命名。
- 新人看不懂老代码 —— 那是重构信号。
不要:专门花一个 sprint"大重构",几乎都会失败 —— 因为业务不停,大重构的 PR review 不动、合并冲突频繁、上线风险高。小步快走是重构的核心节奏。
代码坏味道(Code Smells)
1. 重复代码(Duplicated Code)
同样逻辑在多处出现。改一处忘了改另一处 = bug。重构手法:提取函数(Extract Function)、提取类(Extract Class)。
2. 长函数(Long Function)
50 行以上的函数都该警惕。一般规则:函数应该能在一屏内看完(20-30 行)。重构:把每个"做一件事"的代码块提取成子函数。
// 长函数
function processOrder(order) {
// 验证 (20 行)
if (!order.items.length) throw new Error('empty');
for (const item of order.items) { ... }
if (order.total <= 0) throw new Error('zero total');
// 计算 (15 行)
let total = 0;
for (const item of order.items) { ... }
let tax = 0; ...
let shipping = 0; ...
// 持久化 (10 行)
db.transaction(() => { ... });
// 通知 (10 行)
emailService.send(...);
smsService.send(...);
}
// 重构后
function processOrder(order) {
validate(order);
const totals = calculateTotals(order);
persist(order, totals);
notify(order);
}
function validate(order) { ... }
function calculateTotals(order) { ... }
function persist(order, totals) { ... }
function notify(order) { ... }
3. 大类(Large Class)
一个类几千行,什么都做。违反 SRP。重构:按职责拆分。
4. 长参数列表(Long Parameter List)
// 太多参数
function createUser(name, email, age, city, country, phone, role, active, ...) {}
// 重构 1:参数对象
function createUser(params: CreateUserParams) {}
// 重构 2:Builder 模式
const user = User.builder()
.name(...).email(...).build();
5. 发散式变化(Divergent Change)
"因为数据库换了改这个类,因为 UI 换了也改这个类"—— 这个类有多个变化原因,违反 SRP。重构:按变化原因拆分。
6. 霰弹式修改(Shotgun Surgery)
改一个小需求要改十几个文件。重构:把相关代码集中到一处。
7. Feature Envy
方法 A 里大量使用类 B 的字段 —— 说明 A 应该在 B 里。重构:Move Method。
8. 数据泥团(Data Clumps)
3 个字段总一起出现。重构:把它们封装成对象。
// 三个字段总一起传
function shipTo(street, city, country) { ... }
function billTo(street, city, country) { ... }
// 重构成 Address 类
class Address { constructor(public street, public city, public country) {} }
function shipTo(addr: Address) { ... }
function billTo(addr: Address) { ... }
9. 命名糟糕
fn1()、doStuff()、helper(data)。重命名是最便宜也最有价值的重构。
核心重构手法
Extract Function
从长函数里抽出一段命名清晰的子函数。最常用,几乎天天做。
Inline Function
反向操作:函数太简单,内联回去比单独命名清晰。
Move Method / Move Field
方法 / 字段挪到更合适的类。Feature Envy 的标准修复。
Extract Class
大类拆出新类,把相关字段方法搬过去。
Rename
改变量 / 方法 / 类名。IDE 一键完成,但要审查使用点。
Replace Conditional with Polymorphism
把 switch / if-else 链替换成多态。
// 重构前
function area(shape) {
switch (shape.type) {
case 'circle': return Math.PI * shape.r * shape.r;
case 'square': return shape.side * shape.side;
case 'triangle': return 0.5 * shape.base * shape.height;
}
}
// 重构后
abstract class Shape { abstract area(): number; }
class Circle extends Shape {
constructor(public r: number) { super(); }
area() { return Math.PI * this.r * this.r; }
}
class Square extends Shape {
constructor(public side: number) { super(); }
area() { return this.side * this.side; }
}
Introduce Parameter Object
把多个相关参数合并成一个对象。Data Clumps 的修复。
安全重构的工程实践
1. 重构前先有测试
没有测试的代码不能重构 —— 改完不知道有没有改坏。"特征测试"(Characterization Tests):不评判代码对错,只记录当前行为。重构后这些测试应该全过 —— 即使原行为是 bug。先稳住现状,再渐进改正。
2. 一次只做一种重构
不要边重构边加功能。每次 PR 标清楚是"重构"(行为不变)还是"功能改动"。混在一起 review 极痛苦。
3. 小步提交
每完成一种重构手法就 commit,而不是攒一周再 PR。出问题能精确定位到哪一步。
4. 用 IDE 自动化
Rename、Extract Method、Move Method 这些操作 IDE 都自动且安全(IntelliJ / VSCode / Visual Studio)。手动改容易漏点。
大型重构的策略
Strangler Fig 模式
老代码不能一次替换,逐步用新代码"绞杀"老的:
- 新代码并行实现新版本。
- 把请求逐步路由到新版(灰度发布)。
- 全部流量切到新版后,删老代码。
这种"分支并行"模式让大型重构变成可观测、可回滚的工程任务。Martin Fowler 用"绞杀榕"植物比喻 —— 慢慢从外面包裹老树,等老树死了榕树独立。
Branch by Abstraction
先在老代码上做抽象(接口),老实现仍然在,新实现可以并行加。然后逐步切。比 Strangler Fig 更细粒度。
重构和性能
Fowler 的观点:先重构,再 profile,再优化。理由:
- 重构后的代码更易看懂瓶颈在哪。
- 过早优化(premature optimization)往往优化错地方。
- 清晰的结构让"真正需要优化时"更容易做。
但要分情况:热点路径(每秒百万次)的明显性能问题,可以优化先于重构。
重构什么时候停
- 当下需求不需要这部分代码改 —— 不要"顺手"重构无关代码,扩大 PR 范围。
- 代码已经清楚明白 —— 继续重构边际收益低。
- 没有测试覆盖 —— 风险高于收益。
团队层面的重构文化
- 把"重构"作为正常工作的一部分,不要"等到有时间"。
- Code Review 时主动指出可重构的地方。
- 建立"探测"机制:Linter / SonarQube 自动检测代码坏味道。
- 有重构预算:每个 sprint 留 10-20% 时间给重构和技术债。
- 新人入职任务里包含一次重构:既熟悉代码又留下贡献。
写在最后
重构不是"大动作",是日常习惯。每天写新代码时,留意身边的坏味道,顺手清理一点。三个月后回头看,整个代码库都干净了。这种"积小成多"的力量比"大重构"更可靠 —— 大重构通常会被业务打断,小重构嵌入日常工作流不会被打断。把重构变成肌肉记忆,是工程师成熟的标志。
一图看懂
Strangler Fig 重构流程一图看懂:
—— 别看了 · 2026