重构与 Code Review
写代码和改代码是两种能力。重构是「在不改变外部行为的前提下,改善内部结构」; Code Review 是「让另一个大脑在代码进主线前帮你把关」。这篇把两件事讲成可执行的动作。
一、重构:先建立正确认知
重构 ≠ 重写:
- 重构:小步、行为不变、随时可提交、有测试保护;
- 重写:推倒重来、周期长、风险高。
什么时候重构(Martin Fowler 的三原则):
- 加功能之前:先让结构「准备好」接纳新功能,加起来更省力;
- 修 bug 之前:bug 往往藏在坏味道里,先理顺结构再修;
- Review 之后:评审指出问题,顺手重构。
什么时候不该重构:临近发布、没有测试、结构烂到不如重写 —— 这三种情况,动它比不动更危险。
二、常见坏味道(Code Smell)
| 坏味道 | 特征 | 手法 |
|---|---|---|
| 长函数 | 一屏看不完、嵌套很深 | 提取函数 |
| 大类 | 一个类几十个方法、几千行 | 提取类 |
| 重复代码 | 同一段逻辑出现两次以上 | 提取 + 复用 |
| 魔法数 | if (status == 3) | 命名常量 / 枚举 |
| 过长参数列表 | 方法 5 个以上参数 | 引入参数对象 |
| 散弹式修改 | 改一个需求要动十几个文件 | 把相关逻辑聚到一个模块 |
| 特性依恋 | 一个类频繁访问另一个类的数据 | 把方法搬到数据所在类 |
| 数据泥团 | 几个字段总是成群出现 | 提取成对象 |
| 大 switch | 一个 switch 按类型分派逻辑 | 多态 / 表驱动替换 |
| 注释掩盖坏代码 | 一段难懂的代码配大段注释 | 用命名解释,删掉注释 |
三、核心手法(最常用的五个)
| 手法 | 例子 |
|---|---|
| 提取函数 | 把「一段有名字的逻辑」抽成方法,用名字代替注释 |
| 提取变量 | 复杂表达式抽成有名字的变量 |
| 用多态替换条件 | if (type == "A") ... else if (type == "B") → 策略/子类 |
| 用查询替换临时变量 | 临时变量只在三行里用 → 抽成查询方法 |
| 引入参数对象 | f(a, b, c, d) → f(Params p) |
java
// ❌ 重构前
double getOrderTotal(Order o) {
double base = o.items.stream().mapToDouble(i -> i.price * i.qty).sum();
double discount = o.vip ? base * 0.1 : base * 0.05; // 魔法数 + 条件
double shipping = base > 100 ? 0 : 10;
return base - discount + shipping;
}
// ✅ 重构后
double getOrderTotal(Order o) {
double base = baseAmount(o);
double discount = discountFor(o, base);
return base - discount + shippingFor(base);
}// ❌ 重构前
double getOrderTotal(Order o) {
double base = o.items.stream().mapToDouble(i -> i.price * i.qty).sum();
double discount = o.vip ? base * 0.1 : base * 0.05; // 魔法数 + 条件
double shipping = base > 100 ? 0 : 10;
return base - discount + shipping;
}
// ✅ 重构后
double getOrderTotal(Order o) {
double base = baseAmount(o);
double discount = discountFor(o, base);
return base - discount + shippingFor(base);
}每一步都小:改一处、跑测试、再改下一处。重构出问题时,退一步就能回到可用状态。
四、重构的安全网:测试
没有测试的重构 = 赌博。流程:
- 先给现有代码补「表征测试」(记录当前行为,哪怕没看懂逻辑);
- 重构;
- 测试全绿 = 行为没变。
一句口诀:重构前先让代码可测,重构时只动结构,重构后看测试是否全绿。
五、Code Review:看什么
| 维度 | 具体问题 |
|---|---|
| 正确性 | 边界、空值、并发、异常路径对不对 |
| 安全 | 有没有 SQL 拼接、越权、脱敏缺失 |
| 性能 | 有没有 N+1、循环里查库、大对象复制 |
| 可读性 | 命名、结构、是否一眼能懂 |
| 测试 | 关键路径有没有测、测试是不是只测了 happy path |
| 一致性 | 和项目既有风格、约定一不一致 |
六、怎么提意见(Review 的沟通)
- 对事不对人:说「这段逻辑有个边界问题」,不说「你写错了」;
- 区分阻塞和可选:用标签标明「必须改 / 建议改 / 顺口一提」;
- 给方向不给答案:指出问题,附一句「要不要考虑提取一下」;
- 一次 Review 别太多:超过 200~400 行,Review 质量断崖式下降 —— 提交要小。
Review 的正确顺序:
- 机器先查(格式化、lint、测试、CI)—— 人只看机器查不了的;
- 看「改了什么」的 diff,而不是通读全文件;
- 重点看:接口边界、数据流、安全、并发。
七、Review 与重构的配合
- Review 发现坏味道 → 记下来,别在同一个 PR 里既加功能又大改结构;
- 大型重构要单独一个 PR,只重构、不改行为,Review 起来最省心;
- 团队约定:重构 PR 和功能 PR 分开。
八、速查
| 问题 | 答案 |
|---|---|
| 重构和重写的区别 | 重构小步、行为不变;重写推倒重来 |
| 什么时候重构 | 加功能前、修 bug 前、Review 后 |
| 什么时候别重构 | 临近发布、没测试、不如重写 |
| 最常见的坏味道 | 长函数、大类、重复代码、魔法数 |
| 重构的安全网 | 测试 |
| Review 看什么 | 正确性、安全、性能、可读性、测试 |
| Review 一次多少行 | 200~400 行,再多质量下降 |
| 重构怎么提交 | 单独 PR,和功能分开 |
Cao Note