首页 / 文章 / 软件考古学:解读遗留代码的实用方法

软件考古学:解读遗留代码的实用方法

学习一种循序渐进的方法,用于安全地调查没有文档记录的旧代码库,从分析提交历史到在不影响生产环境的情况下进行重构。

1952 词

每位开发者最终都会经历一种特殊的恐惧感。

你打开一个文件,发现它的行数超过了4,000行。里面没有一条注释,一半的变量名称都是像 x2tempFinal_REAL 这样的玩意儿,而在代码的中间某处还藏着一个名为 doStuff() 的函数,据你判断,它很可能负责整个公司的计费工作。

你使用git blame查看代码来源,结果发现源头是六年前离开公司的人。在Slack上搜索也找不到关于这些代码存在原因的任何信息。唯一可能还记得的人正在外出办公,而且说实话,他们恐怕也对细节已经记不清了。

这就是软件考古学。

它不会出现在任何组织结构图或招聘信息中,但只要你花了一两年以上的时间编写软件,其实就已经在实践这项技能了。你会像田野考古学家在挖掘现场仔细筛选一样,慢慢、专注地研究旧代码,试图弄清楚当初编写它的人的真实想法,尽管他们早已离世,无法再作出解释。

以下是一份关于如何高效完成这项工作、同时避免丧失理智或影响系统运行的指南。

软件考古学究竟是什么?

软件考古学指的是对老旧或无文档记录的代码进行调查、解读和理解,通常目的是为了能够安全地维护它、扩展它,或最终取代它。

这与普通的调试工作不同。调试的前提是你已经了解系统,只是其中某个部分出了故障。而软件考古学则基于相反的前提——你完全不了解这个系统,首要任务就是在敢做任何改动之前建立起对它的理解。

这就好比一名机械师维修自己多年操作的汽车型号,与另一名机械师修复从未拆解过的1962款标致车之间的区别。使用的工具相同,但心态却截然不同。

大多数工程师并非主动选择这类工作,而是逐渐陷入其中。当你开始一份新工作后,六周左右就会有人交给你一张涉及“旧库存系统”的工单。突然间,你不再编写新代码,而是开始进行挖掘工作。

为何这项技能的重要性远超人们认知

有一个令人不安的事实:实际上支撑着全球运行的大多数软件并非新作品。它们十分陈旧,经过多年拼凑而成,只有部分功能被理解,同时也让名义上的负责人暗自忧心。

银行仍在使用20世纪70年代编写的COBOL语言。航空公司的航班调度系统比驾驶这些飞机的飞行员还要老旧。即便是发展迅猛的初创企业,也往往在一两年内就积累了大量遗留代码——这些代码是在截止日期的压力下由已转岗的人员仓促编写而成的,用来解决那些从未被正式记录下来的问题。

如果你只会写新代码,那你就只能从事新项目。但如果你真的能够像侦探勘查犯罪现场那样阅读旧代码,那么当有棘手问题需要解决时,你就会成为工程团队的首选。这是一种很少被公开讨论的专业优势。

考古学家的思维方式

在探讨具体技巧之前,先转变一下视角,这样后续的一切都会更轻松。

假设这段代码在某个时候对某个人来说是有意义的。

这种重新审视的方式比列表中的任何技巧都更为重要。面对那些混乱不堪的代码时,人们很容易认为编写它的人根本不知道自己在做什么。但事实几乎从来都不是如此。更常见的情况是:存在迫在眉睫的截止日期、某些你现在已看不清的限制条件、早已消失的系统要求,或是2016年时看似完全合理的决定,之后却再未被重新审视过。

想象一下走进一座老房子,发现某处有个位置奇怪且看似随意的支撑梁。它看起来毫无用处——直到你得知那里曾经有一面墙,后来那面墙被拆掉了,而这条梁就成了防止屋顶坍塌的唯一支撑。遗留的代码库里到处都是这样的“支撑梁”。在动手修改任何东西之前,你的首要任务就是弄清楚每一条梁究竟在默默支撑着什么。

采用这种思维方式能为你带来两方面的好处:它让你对自己的假设保持谦逊,同时用好奇心取代沮丧情绪。事实证明,好奇心远比恼怒更是一种有效的调试工具。

挖掘遗留代码的技巧

1. 像读日记一样查看提交历史

Git 的历史记录几乎就是最接近真正时间机器的东西。不要只查看文件的当前状态——要追溯它是如何变成现在的样子的。

对某个特定文件运行 git log --follow 常常能揭示出真实的来龙去脉:可能在重要客户演示前仓促添加的函数,周五深夜匆忙提交的修复代码,或是写着“临时解决方案,第三季度发布后删除”的注释,而如今这些都已经过时四年了。

偶然发现了一段针对某个特定客户ID的特殊处理if语句,却没有任何解释。通过git blame查看后,找到了编写该代码的人留下的备注,说明某客户的实际数据中存在拼写错误,这段检查代码只是暂时用来维持系统正常运行,等待该客户修复问题。这个临时解决方案就这样默默存在了五年。了解背后的原因后,团队最终以更谨慎的方式将其移除——他们通过正规的数据迁移操作来处理,而非直接删除检查代码并寄希望于系统不会出错。

2. 关注数据而非仅代码

代码显示的是*可能*发生的情况,而数据则展示的是*实际*发生的情况。

直接去查询数据库,查看真实的记录。如果某个表有status列,其值包含12799这类数值,就不要仅通过阅读代码来试图推断它们的含义——应提取每个值的实际记录,追踪它们在现实中的处理情况。

以一个旧订单表中的type字段为例,代码库中只考虑了1到5这些值,但实际数据中却有数千行记录的type值为0。后来发现0表示“在type字段出现之前就已创建”——这是系统历史的一部分,虽然已从现有代码中消失,但仍清晰地存在于数据之中。

3. 与“幽灵”交流(也就是仍在工作的人)

即便构建系统某部分的开发者早已离世,附近通常仍会有人掌握相关背景信息——可能是最初招募那名员工的人、处理过多年相关工单的支持工程师,或是仍记得“那次事件”的产品经理。

应提出具体且压力较小的问题,而非宽泛的问题。“这段代码是做什么的?”由于过于开放,容易让人猜测。“你还记得2021年左右账单处理退款时有什么异常吗?”这样的问题具体到足以唤起真实的记忆。

4. 在动手之前先绘制地图

考古学家从不会随意开始挖掘——他们首先会绘制遗址地图。对待代码库也应遵循同样的原则。

即使在纸上或文档中粗略地画出数据在系统中流动的实际路径:哪些部分会触发其他部分,哪些部分会将信息存储起来,以及哪些组件依赖于其他哪些组件。你不需要制作出完美的图表——只需有足够的示意图即可避免后续出现意外。

一个实用的方法是:选择某个具体的实际操作,比如“用户取消订阅”,然后从头到尾追踪它,记录它经过的每一个文件和函数。沿着这条路径往往就能了解关于整个系统的大部分关键信息。

5. 重构之前先编写测试

如果某个代码库完全没有测试用例——这在旧系统中很常见——请不要急于一次性修复所有问题。相反,应该编写所谓的特性测试,这类测试仅用于记录代码当前的运行行为,而不论该行为是否正确。

这样做能同时带来两方面的好处:一是为后续的任何变更提供保障,二是强制要求你清晰理解代码,因为只有真正理解了当前行为才能准确描述它。这里的价值很大程度上来自于编写测试的过程,而不仅仅是运行测试本身。

6. 一次只修改一处

一旦你终于理解了一个混乱的系统,就很容易产生一次性通过一个全面的拉取请求来重写整个系统的冲动。千万别这么做。

传统系统往往比看上去更脆弱,原因就在于没有一个人能够完全掌握所有依赖关系。通过逐步进行可逆的小改动——一次一个,每个改动在继续之前都经过验证——才能避免让系统变成又一个令未来考古学家费解的混乱代码版本。

7. 记录下你的发现——为后来者留下线索

凡是你能发掘并理解的内容都值得保存。把它们记在某个地方。哪怕只是一份简短、非正式且不完整的文档,标题定为类似“传统计费服务实际运作方式”之类的,也能为日后继承这些代码的人提供帮助——而那个人很可能是六个月后的你自己,届时你可能已经把一切都忘了。

一个小故事:没人能理解的函数

在某家公司,有一个功能被戏称为“怪物”——它深藏在结账流程中的900行代码,没人愿意去改动。新员工在入职培训时就会听到关于它的警告,一半是开玩笑,另一半则是像当地民间故事一样的真实警示。

最终,有人决定彻底分析它而非回避它。他们没有尝试重写代码,而是追踪实际交易在該功能中的处理过程,梳理出所有的逻辑分支,并为每条路径编写测试用例。整个过程大约花了一周时间。

他们发现的并非混乱——而是一个相当有序的系统,用于处理五种不同的支付提供商,每种都有其独特的特性和边缘情况。这个系统是由有人在实际约束下解决具体问题时编写的,没有事后整理的余地。一旦完全理清其结构,它就不再令人恐惧了。虽然依然复杂,但这种复杂性是有人真正理解的。

本质上,这正是整个学科的精髓:将恐惧转化为清晰的地图。

总结

软件考古学并无什么光鲜之处。没人会在简历上把“擅长解读他人复杂的代码”列为核心技能。然而,它依然是软件工程中最实用却又最被忽视的能力之一。

旧代码不应被当作无用之物丢弃——它记录了在各种压力与限制下做出的决策,而这些背景你可能永远无法完全了解。以好奇心而非评判态度去对待它,你不仅能做出更安全的修改,还会发现这个过程带来的收获远超预期。

所以下次遇到某个文件让你想立刻合上笔记本电脑出门时,先停下来深呼吸片刻。你看到的并非残骸,而是一处等待被理解的遗迹。

拿起画笔,而非推土机。

相关阅读