
身为一名软件工程师,在大型现有代码库中工作是最让人头痛的情况之一。首先我们没法事先演练(任何开源项目在参与体验上都跟这不一样),个人项目也因为体量不大、从头开始而很难体现这种“历史包袱”的沉重感。顺带一提,我这里所说的大型现有代码库,是指那些:
拥有几百万行代码(比如说 500 万行);
有约 100 到 1000 名工程师在同一套代码库上工作;
代码库的最早工作版本至少有十年历史的项目。
我自己在这类代码库上已经工作了十年。下面聊聊那些我真希望年轻时自己能够知晓的经验和心得。
最大的问题来自不一致性
我最深看到的一个致命问题,就是忽略掉代码库的其余部分,只考虑以最合理的方式实现当前功能。换句话说,刻意限制与现有代码库的接触点,以此保证自己的干净代码不受遗留垃圾的污染。对于主要在小型代码库上工作的工程师们来说,这是种很难改掉的习惯——但大家必须努力克服!事实上,为了保持一致性,我们不仅要拥抱遗留代码,还得尽量深入地研究遗留代码库。
为什么一致性在大型代码库中如此重要?因为它能保护我们免受令人讨厌的意外影响、减缓代码库陷入混乱的速度,并允许各位充分运用未来可能出现的改进功能。
假设我们正在为特定类型的用户构建 API 端点,大家可以在端点中旋转一些“如果当前用户不属于此类型,则返回 403 错误”的逻辑。但在动手之前,我们应该先查看代码库中的其他 API 端点在身份验证中发挥何种作用。如果他们使用到一些特定的帮助程序集,那我们也应该使用该帮助程序(哪怕它笨拙、难以集成甚至对于当前用例来说有点用力过猛)。总之,我们必须得控制住自己想要让代码库中的某个小角落变得更加简洁优雅的冲动。
这样做的主要原因,在于大型代码库中往往埋有很多暗雷。例如,你可能不知道代码库中其实存在“机器人”的概念,它跟普通用户类似但又不完全一样,需要特殊处理才能进行身份验证。我们可能不知道代码库中的内部支持工具允许工程师偶尔以用户的角色进行身份验证,而这需要特殊处理才能顺利通过。
总之,大型代码库里肯定还有无数我们难以、甚至根本不可能知晓的情况。现有功能则代表了一条能够穿越雷区的安全路径。如果已经有某个长期存在的 API 端点在以某种方式进行身份验证,那请务必按照同样的路径进行操作,这样我们至少能保证不会被那些暗雷给误伤到。
最重要的是,缺乏一致性正是长期以来困扰大型代码库的核心威胁,因为其导致项目无法进行任何普适性的改进。仍然以之前提到的身份验证为例,如果我们想要引入一种新的用户类型,高度一致的代码库使我们能够直接更新现有身份验证助手集以适应这种需求。
但对于不一致的代码库,不同 API 端点所执行的操作各不相同,我们必须去亲自更新并测试每个实现。实际上,这通常足以把变化扼杀在摇篮当中,或者至少会把更新难度最大的那 5%功能剔除出应用范围——这反过来又会进一步降低一致性,因为现在我们又多了一种只适用于大多数、但并非支持所有 API 端点的用户类型。
所以说,当我们决定认真在大型代码库中搞开发时,应当先深入研究现有技术、并尽可能遵循其基本逻辑。
除了一致性,还有什么重要问题?
一致性就是最重要的,但除此之外,我们也可以快速过一下其他要点:
我们需要对服务在实践中的使用方式(即用户用法)拥有深入了解。比如哪些端点的访问频率最高?哪些端点的重要性最高(即由付费客户使用且无法优雅降级的端点)?服务必须遵循哪些延迟保证,而哪些代码运行在热路径之中?大型代码库中的另一个常见错误,就是进行“微调”——因为这类调整往往会意外触及关注流程的热路径,进而引发大麻烦。
我们绝不能像在小型项目中那样过度依赖自己在开发中测试代码的能力。任何大型项目都会随时间推移而积累下诸多状态(例如,您觉得 Gmail 需要支持多少种用户?)。到了特定的时间点,即使借助自动化手段,我们也不可能测试每一种状态组合。相反,我们能测的就只有关键路径,因此请谨慎编码并领先缓慢发布和持续监控来不断发现问题。
另外,请尽量不要引入新的依赖项。在大型代码库中,新增代码往往会永远存在。依赖项会带来持续的安全漏洞与更高的软件包更新成本,而这些成本几乎肯定会超过特定人员在公司内的任期。即使确有必要,也请确保选择广泛使用且稳定可靠的依赖项,或者是那些在必要时易于分叉的依赖项。
出于同样的理由,一旦有机会可以删减一部分代码,也请务必牢牢把握。这是大型代码库中最危险的工作之一,所以切忌半途而废:首先对代码进行梳理检测以识别生产中的调用者,并将其降至零,这样才能确保可以安全删除。但正如减重对于肥胖人群特别重要,在大型代码库中也没有什么能比安全删除代码更具现实价值。
将工作拆分成多个小型 PR 提交,并通过预加载引导其他团队的代码变更。这一点在小型项目中也有体现,而在大型代码库这边则可谓至关重要。这是因为我们往往会依赖于其他团队中不同领域专家做出的问题预测(毕竟大型项目太过复杂,没有单独哪方能够准确预测所有情况)。如果能够将风险区域的变更保持在较小且易于理解的范围之内,那么这些领域专家就更有可能注意到问题并避免引发事故。
为什么非得接手历史包袱?
最后,我想花点时间强调这些遗留代码库的意义。相信大家都听说过这样一个常见的观点:
为什么非要接手遗留下来的一团乱麻?花时间深入研究错综复杂的代码结构和业务逻辑根本就没有价值。面对庞大的现有代码库,我们的思路应该是拆分出一个个更小、更优雅的服务来进行精简,而不是身陷其中进一步令混乱变得更乱。
我认为这种说法完全错误。主要原因在于,一般来讲大型现有代码库会产生 90%的价值,任何大型科技企业、大部分创收活动(即实际产生经济收益以支付工程开发投入的工作)都来自大型现有代码库。虽然偶有例外,但多数科技巨头的主要业务价值仍然由这些大型现有代码库负责承载和实现。
我也见过那些小巧而优雅的服务能够为某些高收入产品的核心功能提供支持,但实际产品化代码(包括设置、用户管理、计费、企业报告等)仍然离不开大型现有代码库的功能范畴。
所以出于对现有业务运转方式的尊重,我们也不应该因为嫌弃而远离所谓“遗留的混乱”。毕竟这就是我们的职责所在,也是我们必须啃下的工程硬骨头。
另一个原因在于,如果缺少对大型现有代码库的充分理解,我们根本就不可能进行有效拆分。我见过大型代码库的成功拆分案例,但从未见过不擅长在大型代码库上交付功能的团队可以做到这一点。复杂的现实世界告诉我们,人是无法单靠第一性原理就重新设计出任何足够复杂的项目的(即真正能赚钱的项目)。正是无数极其偶然的细节,支撑着巨头们那年均数千万美元的收益。
总结
大型代码库值得我们费心费力,因为我们的工资往往就从它们中来。
到目前为止,最重要的就是保持一致性。
永远不要在未对代码库中现有技术进行深入研究的情况下,盲目启动任何功能。
如果不遵循现有模式,最好找个足够充分且有说服力的理由。
务必理解代码库的生产足迹。
不要指望测试能够覆盖到每一个用例——相反,要充分发挥监控的作用。
把握所有机会、尽量删除代码,但在操作过程中要极度小心。
尽量让其他领域专家能够轻松发现我们的错误。
原文链接:
https://www.seangoedecke.com/large-established-codebases/
评论 2 条评论