构建旧系统:打造可维护系统的艺术
2025年2月6日 | by mebius
作者:来自 ElasticSaman Nourkhalaj
软件开发人员有很多不同的任务,但我们每个人都必须审查旧代码。无论是检tgcode查以前的版本还是查看过去某人如何解决问题,遗留代码都是工作的一部分。但是你是否曾经审查过以前的版本并感到沮丧并问 “谁编写了此代码?” 然后疯狂地点击 “git blame” 来找出谁是责任人?我确信你的答案是肯定的!也许这个负责人是五年前的你,也许是你从未见过的离开公司的同事,甚至是你现在仰慕的队友。
“git blame” 中没有责备的意思。每个人都编写遗留代码是因为一个非常简单的规则:时间。随着时间的推移,需求会发生变化,系统会发展,最重要的tgcode是,人类会学习。因此,今天的最佳状态不可避免地会成为明天的痛苦 —— 尽管,通过向你的团队引入一些做法,这些痛苦可以变得可以忍受。
在我多年的软件开发生涯中,我曾使用过许多遗留系统。有时我很幸运,有机会重新设计和重构,但有时却没有。通过这一切,我了解到遗留系统最重要的方面是其维护。我已经记不清有多少次问过自己 “当我甚至不明白代码的作用时,我到底该如何添加这个新功能或找到错误?”
让未来的 “git blame” 成为一种祝福:做好维护工作
一个可维护的服务具备两个简单却难以实现的特性:可读性和可预测性。
可读性意味着代码的逻辑流畅,能够结合适当的上下文轻松理解。可预测性则意味着代码和架构中存在强有力的模式。即使某个模式不是最佳实践,遵循它也比应对各种随机情况更容易,同时也能促进未来的维护。
作为 Elastic 威胁数据服务团队的一员,我曾多次深入研究过一些年代久远的服务。根据我的经验,以下是构建和维护一个负责任系统的五个最佳实践:
1. 坏的约定比没有约定好
代码约定对可读性的影响比你想象的要大得多。代码通常是你解决问题时的首要或次要参考来源。不论是否有良好的文档支持,拥有明确的模式和约定总是最好的选择。即使这些约定不够理想,你可能会想要修正它们 —— 但不要这样做!
坚持现有的约定和实践可以提高代码的可读性,同时使团队成员的入职过程更快速、更容易。想象一下,你和团队使用的语言中,“咖啡” 表示 “果汁”。如果突然有人开始用这些词的真实含义,那么所有人都会感到困惑,文档变得过时,代码命名和注释也失去意义,每次都需要问:“你指的是 ‘咖啡是咖啡’ 还是 ‘咖啡即果汁’?”在重构或重新设计遗留代码库时,这种情况会变得更加糟糕。
如果你始终知道 “咖啡” 表示 “果汁”,那么遵循这种系统会容易得多。当未来重新设计遗留系统时,你不需要区分两者的含义 —— 你会自然地明白!坏的约定依然是约定,而任何约定都比没有约定要好。
2. 限制你的冒险
科技世界发展的速度比光还快。每年都会出现新的趋势;每周我们都会发现一个新工具;每天我们都会发现一个新的图书馆。对这些事情感到兴奋是很自然的 —— 我们都渴望在下一步工作中探索和使用它们,因为我们渴望做到最好!但是,我们应该在多大程度上将个人冒险融入到这个系统中呢?在被一些令人兴奋的功能迷住之前,让我们先停下来。深呼吸,再想一想 —— 这真的是最好的解决方案吗?我们真的需要这个吗?我们能否利用现有的工具实现同样的目标?
将新工具引入现有系统可能会很棘手。它扩大了调试的范围和发生故障的可能性。它增加了复杂性并给你和你的团队带来更多的上下文切换。我并不是说你永远不应该在系统中添加新工具,但是我们应该谨慎行事,事先彻底检查其优缺点,以确保团队中的每个人都清楚这一点。
3. 测试以求安心
说实话,编写测试并不是一件令人愉悦的事情。这是一个重复、令人沮丧且乏味的任务。但当问题出现时,你会庆幸自己写了测试。单元测试、集成测试和端到端测试等不同类型的测试,能够帮助我们从更好的角度理解系统。有时候,我无法理解一段代码,但直到我找到对应的单元测试,一切才变得豁然开朗。
除了帮助更好地理解代码,测试还降低了生产环境中出错的风险。当然,这仅在测试是最新的,并且在合并或部署更改前作为 CI/CD(Continuous Delivery/Continuous Delivery)的强制步骤运行时才会有效。
乍一看,这似乎显而易见 —— 为什么会有人不运行测试呢?但生活总是充满意外。缺乏经验、交付的紧迫感,以及持续不断的功能需求和问题修复压力,都可能导致测试被忽略。这虽然可以理解,但就像俗话说的,“失去马鞍总比丢掉马好”。永远不要认为补写测试为时已晚,而且正如我常说的那样,从小步开始。
这是我为遗留代码添加测试的方法:
- 找出是否已有测试,并确保它们在你的环境中能够通过。
- 确保测试阶段是 CI/CD 流程的一部分。
- 根据需求,将测试类型从最有益到最不重要排序。
- 将测试纳入团队的代码评审流程。
- 随时编写测试,例如在每个工作周期中添加一组集成测试,或者在修改文件或模块时添加单元测试。优先覆盖代码中重要的部分,而不是追求全面覆盖。
- 随着系统维护,不断改进测试并提高覆盖率。
请记住,测试不是一次性的任务。所以,当你完成某一阶段后,务必提醒自己和团队成员要妥善维护这些测试。
4. 用文档拯救未来
我不会再向你强调文档重要性的问题,因为你可能已经听过无数次了。相反,我想谈谈通常缺失的一部分:历史。
在系统初期开发时,你掌握的信息有限,因此你做出的决定可能从长远来看并不是最佳的。当时的决定是基于你对业务需求的理解、技术专长以及手头工具的知识做出的。而随着时间推移,这三方面都会发生变化,有时还会导致奇怪的补丁出现。我认为我们在文档中缺少的不是对当前状态的描述,而是背后的tgcode故事。
具有历史意识的文档可以为未来的团队提供为什么事情是这样的合理解释。它为上下文提供背景,并将学到的经验传递给下一代开发者。历史记录可以是一段解释、一条链接(指向讨论该变更的 issue)、代码中的注释或其他形式。
我可以分享一个来自我们团队的新鲜例子。几年前,一个 pull request 被合并,但无意中让代码在保存文件时将已经用 bzip 压缩的文件再次用 gzip 压缩了一遍。幸运的是,我们使用的解压库并不在意压缩层数,可以解压到需要的深度,因此这些年来一切正常。直到我们需要实现一个新服务来读取这些文件时,才发现我们资源中隐藏了一个额外的压缩层和一致性问题。
这是一个巨大的漏洞,修复它代价惨重 —— 想象一下替换数万亿个以千字节为单位的小文件。因此,我们不得不保持现状,同时调整新代码以适应逻辑。我们在这段复杂代码的上方添加了注释,解释了其原因,帮助团队所有成员理解并记住了这件事。这还防止了新同事和我们将来再次犯下同样的错误。不要羞于记录你的糟糕决定和错误!
5. 把秘密保存在安全之地
虽然保护敏感数据是常识,但也很容易搞砸。为了避免直接把“钥匙”拱手相让,严格遵循一些规则能帮你节省大量时间,而正确管理你的秘密是其中关键的一步。
秘密(Secrets)可以分为针对个人或针对服务发布。在这两种情况下,请务必使用像 Vault 这样广为人知的秘密管理工具,创建细粒度的访问权限,并根据需要频繁轮换凭据。
关于服务密钥,有一个重要的注意事项:密钥应绑定到服务,而非个人。原因在于,人员会离职或变更团队,他们的权限也会随之更新。如果密钥是绑定到个人的,这可能导致系统出现中断。而为服务颁发密钥可以避免这种情况,同时防止意外授予不必要的权限。
如果这激起了你的兴趣,并且你想了解有关基于角色的访问控制(role-based access control – RBAC)的更多信息,Elastic Cloud 已经以标准且直接的方式处理了 RBAC。你可以查看文档来获得良好的资源。
从内部引领变革
遗留系统可能很粗糙,但并不一定如此。无论你是刚刚加入团队,还是已经工作了一段时间,你都希望保持平静。要找到平静,就找到标准,然后保留并遵循它们 —— 如果它们不存在,就创造它们。
这些技巧将有助于加强你的代码库和社区。我们很高兴通过保持我们的发展进程的开放来加强我们自己的社区。这就是 Elastic 再次开源的原因!
原文:Building a legacy: The art of crafting maintainable systems | Elastic Blog
文章来源于互联网:构建旧系统:打造可维护系统的艺术