Administrator
发布于 2025-05-03 / 2793 阅读
58

AI 辅助下的代码重构实践

3200 行的 OrderService,AI 说拆成六个类

四月初我接手了一个历史模块的重构。OrderServiceImpl 单个文件 3218 行,包含下单、支付回调、退款、履约、对账、通知六种职责,方法之间互相调用,改一个地方要心惊胆战地检查半小时。这活儿我拖了两周没敢动。

后来试着把整个文件喂给 AI 让它给方案,结果出乎意料地可用——但也差点让我搞出一个线上事故。这篇记录完整过程,包括 AI 给的方案哪些能直接用、哪些是坑。

让 AI 给重构方案,但别让它直接改

我用的方式是先让它只分析不动手:把类的方法签名列表(不是全文,太长了)贴给它,让它按职责聚类。

下面是 OrderServiceImpl 的所有方法签名,请按职责把它们分组,指出哪几组适合拆成独立类,并说明分组理由。先不要生成代码。

它给出的分组跟我自己心里的判断基本一致,但有一处分得比我更合理:它把"对账"单独拆出去了,理由是"对账是独立触发的定时任务,跟订单主流程的调用链没有交集"。这一点我之前没想清楚,打算把对账和履约放一起。

但它的方案里也有一处明显错误:它把 cancelOrderrefund 归到了同一个类。实际上这两个方法虽然都涉及"把钱退回去",但一个是未支付取消(不涉资金),一个是已支付退款(要调支付网关),放一起迟早出事。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 起的名字过于泛化,比如 processDatahandleInfo)。

卫语句改造

把嵌套 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();
}

看起来一模一样,对吧?完全不等价。BigDecimalequals 会比较精度,new BigDecimal("100.00")new BigDecimal("100.0")equals 比较是 false,用 compareTo 是 0。我们系统里订单金额是 2 位精度,支付网关回调的金额是 4 位精度,用 equals 会导致所有正常订单都抛异常

这个改动在 code review 时滑过去了,因为两行代码长得太像。是集成测试里一个"支付回调精度不一致"的用例把它抓出来的——那个用例还是我补测试时顺手加的边界场景。

事后我总结了一条规矩:涉及 BigDecimal、时间、浮点数、集合顺序、并发语义的改动,AI 生成的代码必须逐字符比对原文。AI 对这类"看起来等价但实际有陷阱"的替换毫无感知,因为它不理解精度、时区和比较语义。

同一轮里还发现另一个问题:AI 把一个 Listfor 循环改成了 stream,但原循环里有对 null 元素的跳过逻辑,改造后 .map() 里抛了 NPE。这也是纯靠测试兜住的。

我们的重构节奏

最终采用的节奏是小步 + 每步验证,一共拆成 14 个 PR:

  1. 补测试(3 个 PR,不动业务代码);
  2. 纯结构调整:提取方法、改名、卫语句(4 个 PR,零行为变化);
  3. 抽接口 + 依赖倒置(2 个 PR);
  4. 按职责拆类(3 个 PR);
  5. 删除无用代码、整合重复逻辑(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 辅助下的代码重构实践》本身不算多难,难的是线上真出问题那十分钟里的判断。经验都是这么来的。

参考