跳转到内容

一次真实的重构:从 3000 行到 800 行

约 1371 字

一次真实的重构:从 3000 行到 800 行

讲一个我经手的真实重构(细节脱敏)。主角是个 订单模块:三年历史、五个作者、3000 行的 order.service.js。最后变成 800 行 + 五个清晰的小模块。 过程没有神话,全是土办法。

一、动机:三次「改到崩溃」

引爆点是三次事故:

第一次:加「部分退款」,改了状态机,
       结果「全额退款」的分支被影响,线上错退 3 单
第二次:改金额计算(加优惠券),
       改了 A 处忘了 B 处,两处逻辑不一致
第三次:性能优化(查订单列表),
       发现一个函数里嵌了 6 层循环 + 嵌套查询

每次都是「就改一小处」的诉求,每次都引出大坑。 团队共识:不是人不行,是这块代码到了 「改不动」的临界点。

二、动手前:两件事

1. 补测试( Fowler 的规矩)

先给核心路径补测试(不追求全覆盖):

  • 下单(各种商品类型组合);
  • 状态流转(每个状态 + 每个转移);
  • 金额计算(优惠券/折扣/运费组合)。

花了一周,写了几十个测试。 期间就发现 两个「现有行为和文档不一致」的 bug —— 这说明测试开始值回票价了。

2. 画现状(理解先于动手)

把 3000 行读一遍,画调用图:
  入口(3 个 API)→ 校验 → 计价 → 落库 → 通知
  发现:
  - 「校验」逻辑散落在 5 个函数里,互相调用
  - 「计价」在一个 400 行的函数里,
    优惠券/活动/运费三种逻辑缠在一起
  - 「通知」同步发送,拖慢主流程

画完图,重构方案自然浮现 —— 不是设计出来的, 是「看清了之后」知道该往哪去。

三、重构:小步走,六次 PR

没搞「重构大月」,分六个 PR,每个可独立上线:

PR 1:抽取「校验」模块(纯搬砖,不改逻辑)
      测试全绿 → 合
PR 2:抽取「计价」模块,拆三个子函数
      测试全绿 → 合
PR 3:修 PR 2 中暴露的「两处逻辑不一致」bug
      加测试 → 合
PR 4:通知改异步(队列)
      测试 + 观察 → 合
PR 5:删死代码(300 行没人调的)
      测试全绿 → 合
PR 6:状态机收敛成一个表驱动
      测试全绿 → 合

关键纪律:

  • 每个 PR 只做一类事(「抽取」和「改行为」 绝不混在一个 PR —— 混了,出问题说不清是谁的锅);
  • 每个 PR 都跑全部测试(绿的才合);
  • 每个 PR 都可回滚(出问题回一个,不用回六个);
  • PR 之间间隔几天(观察线上,别连发)。

四、踩的坑

坑一:「顺手优化」 PR 2 里我顺手把一个循环改成了 map/filter, 看着优雅 —— 但行为和原来不一致 (原来会跳过 null,我的不会)。测试抓住了, 但我花了半小时才想明白「为什么原来的行为 其实是故意的」(兼容脏数据)。 教训:重构 PR 里,优雅感是敌人。

坑二:删了「没人调」的代码 PR 5 删函数时,全局搜没找到调用 —— 删了。 上线一周后,一个定时任务挂了: 那个函数是字符串反射调用的 (global[funcName]()),静态搜索看不见。 教训:「没人调」要加限定 —— 静态搜索没人调。 (后来靠 log 监控才发现。)

坑三:低估联调成本 以为「行为不变」的纯重构,前端不用动。 结果有个字段的顺序变了(对象 key 顺序), 某处前端依赖了这个顺序渲染。测试没覆盖 前端渲染,它不知道。教训:重构的影响面, 不止测试覆盖的那部分。

五、结果与复盘

代码:3000 行 → 800 行(+ 600 行测试)
性能:订单列表 P99 从 2.3s → 180ms(异步化 + 循环优化)
后续:加新状态从「半天+提心吊胆」→「一小时+有测试兜底」
Bug:上线后一个月,订单模块 0 事故

事后复盘的三个「下次还会做」:

  1. 先补测试再动手 —— 没测试的重构是裸奔, 这次没它,PR 2 就翻车了;
  2. 小 PR + 可回滚 —— 六个小 PR 的风险 远小于一个大 PR,这是纪律不是风格;
  3. 画图 + 搜索 + log 三重确认「没人调」 —— 反射/动态调用是静态分析的盲区。

一个「下次要改」:联调(前端+后端+定时任务) 应该更早介入 —— 我在最后一周才发现前端依赖, 应该在 PR 1 之后就同步各方。

六、写给要重构的你

如果看完心动了,先记住三句话:

  1. 「代码跑着就别动」和「天天改到崩溃」之间, 隔着的是测试和小步;
  2. 重构不是重写 —— 我没推翻任何设计, 只是「把缠在一起的拆开」。推翻重来的 大概率是自嗨;
  3. 重构的价值不在代码行数(3000→800 只是结果), 在「以后敢改」 —— 这个敢字, 是给团队的定心丸。

小结

一次重构的实录:动机(三次事故)→ 补测试 → 画图 → 六个小 PR → 三个坑 → 复盘。 没有银弹,全是纪律。想系统学手法, 看 《重构》读书笔记; 想动手了,回来看看这篇的「纪律清单」。