3200 行的 OrderService,AI 说拆成六个类
四月初我接手了一个历史模块的重构。OrderServiceImpl 单个文件 3218 行,包含下单、支付回调、退款、履约、对账、通知六种职责,方法之间互相调用,改一个地方要心惊胆战地检查半小时。这活儿我拖了两周没敢动。
后来试着把整个文件喂给 AI 让它给方案,结果出乎意料地可用——但也差点让我搞出一个线上事故。这篇记录完整过程,包括 AI 给的方案哪些能直接用、哪些是坑。
让 AI 给重构方案,但别让它直接改
我用的方式是先让它只分析不动手:把类的方法签名列表(不是全文,太长了)贴给它,让它按职责聚类。
下面是 OrderServiceImpl 的所有方法签名,请按职责把它们分组,指出哪几组适合拆成独立类,并说明分组理由。先不要生成代码。
它给出的分组跟我自己心里的判断基本一致,但有一处分得比我更合理:它把"对账"单独拆出去了,理由是"对账是独立触发的定时任务,跟订单主流程的调用链没有交集"。这一点我之前没想清楚,打算把对账和履约放一起。
但它的方案里也有一处明显错误:它把 cancelOrder 和 refund 归到了同一个类。实际上这两个方法虽然都涉及"把钱退回去",但一个是未支付取消(不涉资金),一个是已支付退款(要调支付网关),放一起迟早出事。AI 只看得见方法名和调用关系,看不见业务语义。
动手前先补测试,这步不能省
这是我在这次重构里最正确的一个决定。在动第一行代码之前,我花了三天给 OrderServiceImpl 补集成测试,用的是 Testcontainers 起真实 MySQL + Redis,覆盖六个职责的主流程和主要异常分支。
补测试的过程本身也用到了 AI——让它根据方法实现生成测试用例清单,我挑有用的写断言。这里必须注意:AI 生成的测试断言经常是无效的。它特别喜欢写这种:
// AI 生成的,毫无意义
assertNotNull(result);
assertEquals(order.getAmount(), result.getAmount()); // 直接从入参拿,等于没验
我最后写的断言都是自己重新算一遍期望值,不复用被测代码里的任何逻辑。覆盖率从 12% 提到 71%,其中核心的金额计算和状态流转部分到了 89%。
这三天决定了后面所有重构敢不敢做。没有这 71% 的覆盖率,我一步都不敢迈。
AI 真正好用的重构场景
实测下来,下面这几类重构 AI 做得又快又准:
提取方法和方法改名
一个 180 行的方法里有五段逻辑,让 AI 提取成五个方法并给出命名,准确率很高。我批量处理了 23 个长方法,人工检查只改了 4 个命名(AI 起的名字过于泛化,比如 processData、handleInfo)。
卫语句改造
把嵌套 if-else 改成卫语句,AI 做得比我快得多,而且能保持逻辑等价。这类纯结构调整是它最擅长的:不改变执行语义,只改变代码形状。
// 改造前
public Result submit(Order order) {
if (order != null) {
if (order.getItems() != null && !order.getItems().isEmpty()) {
if (stockCheck(order)) {
return doSubmit(order);
} else {
return Result.fail("库存不足");
}
} else {
return Result.fail("订单项为空");
}
}
return Result.fail("订单为空");
}
// AI 改造后(逻辑等价,已验证)
public Result submit(Order order) {
if (order == null) return Result.fail("订单为空");
if (order.getItems() == null || order.getItems().isEmpty())
return Result.fail("订单项为空");
if (!stockCheck(order)) return Result.fail("库存不足");
return doSubmit(order);
}
重复代码识别
订单状态和退款状态各有一套枚举转换逻辑,散落在六个地方,实现有细微差异。AI 找出来了并合并成一个工具类。这个我确实没注意到——人眼在 3200 行里找重复太难了。
差点搞出线上事故的一次
下面这个必须单独说。重构金额校验逻辑时,AI 把一段代码"优化"成了这样:
// 原始代码
if (order.getAmount().compareTo(paid.getAmount()) != 0) {
throw new AmountMismatchException();
}
// AI 重构后
if (!order.getAmount().equals(paid.getAmount())) {
throw new AmountMismatchException();
}
看起来一模一样,对吧?完全不等价。BigDecimal 的 equals 会比较精度,new BigDecimal("100.00") 和 new BigDecimal("100.0") 用 equals 比较是 false,用 compareTo 是 0。我们系统里订单金额是 2 位精度,支付网关回调的金额是 4 位精度,用 equals 会导致所有正常订单都抛异常。
这个改动在 code review 时滑过去了,因为两行代码长得太像。是集成测试里一个"支付回调精度不一致"的用例把它抓出来的——那个用例还是我补测试时顺手加的边界场景。
事后我总结了一条规矩:涉及 BigDecimal、时间、浮点数、集合顺序、并发语义的改动,AI 生成的代码必须逐字符比对原文。AI 对这类"看起来等价但实际有陷阱"的替换毫无感知,因为它不理解精度、时区和比较语义。
同一轮里还发现另一个问题:AI 把一个 List 的 for 循环改成了 stream,但原循环里有对 null 元素的跳过逻辑,改造后 .map() 里抛了 NPE。这也是纯靠测试兜住的。
我们的重构节奏
最终采用的节奏是小步 + 每步验证,一共拆成 14 个 PR:
- 补测试(3 个 PR,不动业务代码);
- 纯结构调整:提取方法、改名、卫语句(4 个 PR,零行为变化);
- 抽接口 + 依赖倒置(2 个 PR);
- 按职责拆类(3 个 PR);
- 删除无用代码、整合重复逻辑(2 个 PR)。
每个 PR 都要求 CI 全绿(单元测试 + 集成测试 + 静态扫描),并且第 2 步的四个 PR 我额外做了一件事:用字节码对比工具验证重构前后方法体的逻辑等价性。这听起来很重,但其实就是在 CI 里加了一步,对纯结构调整的 PR 特别有效。
第 4 步拆类时,我让 AI 生成的拆分代码基本只当草稿用,自己重写了类之间的依赖注入和事务边界划分。这部分 AI 做不好——事务边界是它完全看不见的东西。
耗时和效果
| 项目 | 数值 |
|---|---|
| 总耗时 | 11 个工作日 |
| 其中补测试 | 3 天 |
| 代码行数 | 3218 → 6 个类共 2140 行 |
| 最大单类行数 | 3218 → 487 |
| 测试覆盖率 | 12% → 78% |
| 重构引入的缺陷 | 3 个(全部被测试拦截) |
| 上线后相关故障 | 0 |
如果纯手工做,我估计要 20 个工作日以上,主要省在提取方法、改名、找重复这三块。但补测试那三天是纯增量投入,以前这种"先补测试再重构"我多半会跳过,AI 让重构本身变快之后,才有时间做正确的事。
几条经验
- 让 AI 出方案,但不要让它执行完整重构。它的方案可用率大概 70%,执行准确率更高但一旦出错就是隐蔽的语义错误;
- 测试覆盖率是重构的前提而不是成果。这次 3 个被拦截的缺陷里,有 2 个是那种"看代码绝对看不出来"的类型;
- 分批提交,每批可独立回滚。我见过有人一次性提交 3000 行重构,出问题排查到怀疑人生;
- AI 最擅长"不改变语义的形状调整",最不擅长"涉及业务判断和数值语义的改动"。按这个边界分配任务;
- 金额、时间、并发、集合顺序这四类改动,永远自己写。
写在后面
现在回头看,《AI 辅助下的代码重构实践》本身不算多难,难的是线上真出问题那十分钟里的判断。经验都是这么来的。