第37章:处理屎山代码:先织网,再动刀

"To me, legacy code is simply code without tests."

"在我看来,遗留代码就是没有测试的代码。"

—— Michael Feathers,《Working Effectively with Legacy Code》

第 36 章教你把陌生的代码库读成一门课。这一章面对存量里最难缠的一种:屎山。没测试、没文档、作者离职、没人敢动,业务还全靠它跑。AI 时代这类代码反而成了主战场:读它的成本第一次降到这么低,动它的人也第一次这么多。

37.1 屎山为什么动不了

Feathers 在《Working Effectively with Legacy Code》里给过一个反直觉的定义:遗留代码就是没有测试的代码。技术旧、代码丑都只是表象,决定性的一条是没测试。没有测试,就没有行为快照;没有行为快照,任何一次改动都可能悄悄改变系统行为,改对改错全凭运气。于是团队形成共识:别碰,能跑就行。屎山因此越长越大。

它还有几个帮凶。作者早离职了,没人知道某个分支为什么这么写;文档停留在 3 个大版本之前;模块互相纠缠,改一处,崩一片。维护它的日常是考古:看提交记录,猜当年意图。

AI 时代给这个循环加了一台鼓风机。生成和理解的速率差(第 36 章)在 Agent 身上被放大:一夜之间堆出一个模块,没有测试就进了主干。Garry Tan 把这种项目叫做「鬼屋」,每次改东西都可能碰倒别的房间的东西。屎山的供应端在加速。

但天平的另一端也在动。AI 是最有耐心的读者:一个方法 800 行,switch 里嵌 switch,人会绕着走,Agent 读十遍都一样。理解屎山的人力瓶颈没了,维护屎山从纯苦力活,变成一门有方法论的工程。

37.2 先织安全网:AI 补测试

动屎山的第一条纪律:没有安全网,不动手。安全网是一种特殊的测试,特征测试(characterization test),也叫黄金快照(golden master)。做法是先不评判对错,把系统的现状原样冻住:输入 X,得到输出 Y,全部记录下来。之后任何改动跑一遍快照,行为变了就报警。

写新代码的 TDD 先写测试定义期望行为;特征测试反过来,把既有行为原样冻结,哪怕它本身是错的。Agent 时代有一场 TDD 还灵不灵的争论,实验结论是 Agent 写新代码时先做全局设计的版本质量更高、还省三倍 token。这场争论不影响特征测试,两者干的本来就是两件事。冻结现状反而是 AI 最擅长的活:它读代码推出行为,批量生成特征测试,人只抽查几份。

测试有了,再装一个棘轮:质量基线只许升,不许降。Garry Tan 的开源项目 GStack 和 GBrain 有约 97 万行代码、665 个测试文件,90% 的覆盖率靠 Agent 写代码时顺手写测试做到,几乎不额外花人力。每个 Agent 踩过的坑变成一条永久规则,下次不再犯。补测试不追求一次到位,它给之后所有改动上保险。

37.3 画地图:读懂没人懂的代码

安全网之外的第二件事是搞懂现状。屎山最大的未知数是没人说得清它现在到底做了什么,文档早过期了,唯一的事实来源是代码本身。

上一章的全套方法在这里直接复用:/teach 把代码库读成一门课,/understand 指着代码逐段讲解,代码知识图谱(第 21 章)把调用关系铺开,UML(第 22 章)把结构画成图。差别只在对象:第 22 章画 AI 生成的代码,这里画十年前的遗产。工具一样,心态不一样,读屎山时连提问都是考古式的:这段逻辑是在防什么?

最早一批做「AI 读屎山」的产品从最硬的 COBOL 起家。bloop(9.5k star)的主打功能是对着 COBOL 代码库提问,后来转型代码搜索,2024 年归档。方向是对的,它只是起跑太早。

37.4 小步渐进:绞杀者、长芽和棘轮

有了网和地图,开始动。三种被反复验证的走法,按战场规模从小到大排列。

第一种是长芽法(sprout method),Feathers 的手法。往屎山里加功能时,新逻辑写成独立的小方法、小类,长在旧代码外面,只在接缝处嫁接进去。旧代码一行不动,新代码自成一体、自带测试。适合改动小的场景。

第二种是绞杀者模式(strangler fig),Fowler 2004 年命名,名字来自澳大利亚的绞杀榕:种子落在树上,慢慢长成自己的根系,最后取代整棵树。落到工程上:沿旧系统边缘盖新系统,新功能只写进新系统,旧功能按块搬迁,新旧之间用门面衔接,搬完的地方旧壳拆除。适合服务级的屎山,边跑边换轮胎。

第三种是棘轮(ratchet),它管节奏:每次改动只许变好,不许变坏。覆盖率和规则只进不退,第 15 章的复利工程,管的就是这件事的编排。

具体动刀时,第 35 章的 /refactor 五阶段安全协议正好派上用场:加测试、动手、跑测试、审 diff、确认行为没变。第 23 章的手法目录提供单步操作,这一章提供作战地图,两层配合。

37.5 大规模现代化:把屎山批量翻新

单个屎山靠上面的流程,几千个屎山要靠工厂。厂商已经交出成绩单。

Amazon Q Developer 的代码转换 agent 是目前最大的公开案例:亚马逊内部 3 万个生产应用从 Java 8/11 升到 17,省下超过 4,500 个开发人年,每年节省 2.6 亿美元(2024 年 Q2 财报电话会披露)。人工升级一个依赖平均要一天,agent 只要几分钟。它的工作方式分几步:先分析代码和依赖,生成一份转换计划;然后逐步改代码,升级废弃的依赖;最后构建验证,交一份改动清单。

IBM 挑了最硬的骨头:COBOL。全球约 70% 的银行交易跑在 COBOL 主机上,会写 COBOL 的工程师在陆续退休。watsonx Code Assistant for Z 用生成式 AI 把 COBOL 服务翻译成 Java,让主机工程师带着 AI 一起搬家。

开源侧另有两条路线。OpenRewrite(3.7k star)走确定性:重构写成 AST 级的配方(recipe),一次跑遍成千上万个仓库,Spring、Quarkus 的版本升级都有官方配方,不碰运气。codemod(1k star)把代码迁移做成可分享的 CLI,内置 ast-grep 支持。微软开源了 COBOL 到 Java(Quarkus)的多 agent 现代化样例(218 star),几个 agent 分工完成认知升级的各环节。

这些方案拼出一个共同规律:机械转换交给确定性工具,语义判断交给 LLM,验证交给测试。三者缺一,翻新工厂就会批量生产新屎山。

37.6 重写还是重构

绕不开的选择。2000 年 Joel Spolsky 写下《Things You Should Never Do, Part I》,批评 Netscape 把浏览器从零重写:旧代码扔掉重来的几年里,用户跑光了,浏览器大战输了。他的理由很硬:旧代码里沉淀着无数次线上事故换来的修复,每一行怪代码背后都有一个没人记得的坑。重写等于把这笔遗产清零。系统知道的东西,程序员早忘了。

AI 时代这笔账要重算一半。重写的成本降了一个量级,用 AI 重写一个模块从按月变成按周。但 Spolsky 的核心论点一个字都没变:领域知识埋在旧代码里,不在任何文档里。

AI 给出的新解法是先挖后写:让 Agent 先读透旧代码,把行为抽成特征测试,把规则抽成文档,把规格完整立在纸面上。之后再决定这条线是重构还是重写。判断依据从「感觉写不动了」变成「规格已经能完整复述」,重写的风险从失控变成可控。

一条实用的决策线:行为有测试兜底,渐进重构;行为说不清、测试织不起来,先补测试再动手;新旧技术栈完全不兼容、代码量级可控,带着规格重写。至于什么都不准备就推倒重来,在屎山上输的次数远比赢的多。

37.7 本章小结

至此第三部分讲完:从需求规划到编排,从审查到屎山,AI 时代的存量工程有了整套打法。下一部分进入前端和设计。