面对大型、成熟的代码库,“动手”优化之前一定要三思而后行。所谓的“遗留代码”并不等同于“烂摊子”,它往往承载着核心业务的运行基础。与个人项目或开源项目不同,这类代码库可能拥有数百万行代码、数百名工程师协作,并经历了十年以上的业务迭代,复杂度之高犹如“雷区遍布”,以下是软件工程师结合自身十多年开发经验总结的一些宝贵建议。
以下为译文:
在大型、成熟的代码库中工作是软件工程师最难掌握的技能之一。因为你无法提前做准备,即使深根于开源项目也无法提供同样的经验。因为个人项目往往规模较小,而且从零开始,所以它们教不了你如何应对复杂的代码库。这里所说的“大型、成熟的代码库”一般是指:
大约有 500 万行代码
有 100 到 1000 名工程师在同一个代码库上工作
代码库最初的版本至今已经有十年的迭代了
我已经在这种代码库中工作了十年。以下是我希望当初有人告诉我的一些经验。
最大的错误是前后矛盾
在大型代码库中,我见过最多、最致命的错误就是:忽略现有代码库的风格,仅按照自己认为最合理的方式实现功能。换句话说,尽量减少与现有代码的交互,保持自己写的代码整洁、不受遗留代码的“污染”。对于习惯于小型代码库的工程师来说,这种做法很自然,但你必须避免!相反,你需要尽可能深入现有的遗留代码库,以保持一致性。
为什么一致性在大型代码库中如此重要?
因为一致性可以帮助你避免意想不到的问题,减缓代码库变得混乱的速度,还能为未来的改进留出空间。
举个例子:假设你要为特定类型的用户创建一个 API 接口。
你可以简单地在接口中添加“如果当前用户不是这种类型,就返回 403”的逻辑。但你更应该先看看代码库中其他 API 接口是如何处理权限验证的。如果它们使用了某些特定的工具或方法,你也应该使用那些工具(即使它们看起来很丑、难以集成,或者对你的场景显得过于复杂)。你必须抵抗让你负责的这部分代码比整个代码库“更优雅”的冲动。
一致性的重要性还在于,大型代码库往往有许多隐藏的问题。比如,你可能不知道代码库里有“机器人用户”这种概念,它们和普通用户很相似但需要特殊的权限处理;或者你可能不知道代码库中的内部支持工具允许工程师以用户身份认证,这也需要特殊处理。如果你按照现有功能的做法来实现,就可以避免踩到这些“雷区”,而不必了解代码库中所有复杂的细节。
另外,从长远来看,缺乏一致性是大型代码库的主要杀手。它会让代码库很难进行通用的改进。继续权限验证的例子:如果你想引入一种新类型的用户,一个一致的代码库允许你只需更新现有的权限工具即可支持新用户类型。而在不一致的代码库中,每个接口的实现都不同,你需要逐一更新和测试每个实现。这种情况下,通用改进往往会被搁置,或者一些难以更新的部分会被忽略,进一步加剧不一致性。
因此,在大型代码库中实现任何功能之前,一定要先找找现有的实现方案,并尽可能遵循它们。
还有哪些需要注意的?
最重要的还是保持一致性,但除此之外,还有一些需要特别注意的点:
1. 理解服务的实际使用情况
你需要清楚用户是如何使用这个服务的。例如,哪些接口被调用得最多?哪些接口最关键(比如,付费客户必须使用且不能轻易出错的)?服务有哪些延迟要求,哪些代码会出现在关键路径中?
大型代码库中常见的一个错误是对代码做一个“很小的调整”,却意外地影响了关键路径上的重要流程,导致严重问题。
2. 不要完全依赖开发环境中的测试
在小项目中,开发环境的测试可能已经足够,但大型项目中,随着时间推移,代码库会积累大量状态(比如,你觉得 Gmail 支持多少种不同类型的用户?)。到了一定程度,即便有自动化测试,也无法测试所有状态组合。
你需要重点测试关键路径,采用防御性编码策略,并依赖慢速上线(slow rollout)和监控来发现问题。
3. 尽量避免引入新依赖
在大型代码库中,代码往往会“永远”存在。新依赖会带来持续的维护成本,包括安全漏洞和包更新,可能远远超出你的在职时间。
如果非要引入依赖,请选择那些广泛使用且可靠的,或者容易在必要时进行分叉(fork)的。
4. 抓住机会删除代码
如果有机会删除代码,一定要好好把握!虽然这是大型代码库中最有风险的工作之一,但也是最值得的。
在删除代码之前,要确保做足准备:先给代码加监控工具,确认哪些调用者还在生产环境中使用,然后逐步减少到零,确保删除是安全的。尽管过程麻烦,但清理代码对于维护代码库的健康至关重要。
5. 小步提交,并优先处理会影响其他团队的改动
这一点在小项目中也很重要,但在大型项目中尤为关键。因为你需要依赖其他团队的领域专家来帮你发现可能遗漏的问题(大型项目太复杂,无法靠一个人提前预见所有问题)。
如果你的改动涉及风险区域,一定要保持提交小而易读,这样领域专家才能更好地发现潜在问题,避免出现事故。
为什么要花时间在这种“遗留代码”上?
最后,我想为这些大型代码库辩护一下。有人常常会有这样的想法:
为什么要在这种遗留代码的“泥潭”里挣扎?花时间处理这些“意大利面条”式的代码固然难,但这并不算是优秀的工程实践。面对庞大的旧代码库,我们的任务应该是将其拆分成小而优雅的服务,而不是继续在这堆混乱中添砖加瓦。
我认为这种观点完全站不住脚。
主要原因是:大型代码库通常贡献了90%的业务价值。
在任何一家大科技公司,大部分能带来收入的工作(也就是支付你工程师薪水的那部分)都依赖于一个庞大且已存在多年的代码库。如果你认为你的公司例外,那也许你是对的,但我只有在你对那份大型代码库非常熟悉的情况下,才会认真对待你的意见。
我见过很多例子:一个小巧优雅的服务可能是某个高收入产品的核心功能,但所有真正让产品化得以实现的代码——比如设置、用户管理、计费、企业级报告等——仍然扎根在那个庞大的代码库中。
所以,你需要学会如何在这个“遗留泥潭”里工作,因为这才是你公司实际的核心业务。不管它是不是“好的工程实践”,它都是你的工作职责。
另一个原因是:你无法在不了解大型代码库的情况下对其进行拆分。
我确实见过大型代码库被成功拆分,但从未见过一个对代码库不熟悉的团队能够完成这个任务。你不可能用“从零开始”的方式去重新设计一个复杂的项目(比如一个能带来实际收入的项目)。因为支持数千万美元收入的项目中,有太多细节是意外形成的,但却不可或缺。
总结
1. 大型代码库值得投入精力,因为它们通常是公司收入的主要来源,也就是它们支付了你的工资。
2. 一致性是最重要的原则,不要忽视它。
3. 不要盲目上手新功能,先研究代码库里是否有类似的实现可以参考。
4. 如果你打算不遵循现有的模式,那就需要一个非常充分的理由。
5. 理解代码库在生产环境中的使用方式,哪些代码路径是关键,哪些接口使用最频繁。
6. 别指望能测试每种情况,要学会依赖监控和渐进式上线策略。
7. 只要有机会删除代码,就抓住机会,但一定要谨慎操作。
8. 尽量让领域专家能快速发现你的问题,比如小步提交、清晰代码变更,让团队合作更高效。