概述
本章探讨了依赖管理这一软件工程中最具挑战性的问题之一。依赖管理涉及对库、包和外部依赖网络的管理,这些依赖通常不在我们的控制范围内。与源码控制不同,依赖管理需要处理外部变更带来的风险,例如版本更新、安全漏洞和依赖冲突。本章分析了依赖管理的复杂性,并探讨了现有解决方案及其局限性,同时分享了Google在依赖管理方面的实践和经验。
主要内容
1. 依赖管理的挑战
- 版本更新与描述:如何在不同版本的外部依赖之间进行更新?如何描述版本之间的差异?
- 依赖变更的类型:哪些类型的变更在依赖中是被允许或预期的?
- 依赖决策:何时依赖其他组织的代码是明智的?
- 依赖网络的复杂性:依赖管理的复杂性在于处理整个依赖网络的变化,而不仅仅是单个依赖。
2. 为什么依赖管理如此困难?
- 依赖网络的动态性:依赖网络中的每个节点(库或包)都会随着时间推移更新,这些更新可能导致版本冲突。
- 冲突需求和钻石依赖问题:当两个依赖对同一底层依赖有不同的版本要求时,可能会出现无法解决的版本冲突。例如,
liba
和libb
都依赖于libbase
,但版本不兼容,导致libuser
无法同时满足两者的需求。
3. 导入依赖
- 重用与开发成本:导入现成的依赖通常比重新开发更高效,但需要考虑长期维护成本。
- 兼容性承诺:依赖提供者应明确其兼容性承诺,例如C++标准库的ABI兼容性、Go的源码兼容性,以及Google Abseil的有限API兼容性。
4. 导入依赖时的考虑
- 测试和维护:依赖是否有测试?是否有人负责维护?
- 兼容性:依赖的兼容性承诺是什么?
- 流行度和维护频率:依赖的流行度和更新频率如何?
- 内部问题:在Google内部,我们如何维护和更新导入的依赖?
5. Google如何处理依赖导入
- 内部依赖管理:大多数依赖是内部开发的,因此依赖管理更像是源码控制问题。
- 外部依赖管理:外部依赖被导入到
third_party
目录,但缺乏足够的维护和更新机制。
6. 依赖管理理论
- 无变更模型(Nothing Changes):假设依赖永远不会改变,虽然简单,但不可持续。
- 语义化版本(Semantic Versioning, SemVer):通过主/次/补丁版本号来管理依赖,但存在局限性。
- 捆绑分发模型(Bundled Distribution Models):通过分发商来管理依赖集合。
- 始终处于最新状态(Live at Head):依赖始终指向最新版本,并通过持续集成(CI)确保兼容性。
7. SemVer的局限性
- 版本号的主观性:版本号是依赖维护者对变更风险的估计,而非绝对承诺。
- 过度约束(Overconstraining):SemVer可能导致不必要的版本冲突。
- 过度承诺(Overpromising):SemVer可能低估变更的实际风险。
- 最小版本选择(Minimum Version Selection, MVS):通过选择最小版本更新来降低风险。
8. 依赖管理与无限资源
- 假设无限资源:如果计算资源无限,依赖管理可以通过运行下游依赖的测试来验证变更的安全性。
- 实际限制:目前,测试资源有限,依赖管理需要更高效的策略。
9. 导出依赖
- 开源与维护:开源项目需要长期维护,否则可能导致声誉损失。
- 案例研究:Google在开源
gflags
库时遇到的挑战,以及AppEngine服务中Python运行时更新带来的兼容性问题。
总结
依赖管理是一个复杂且动态的问题,需要在维护成本和变更风险之间找到平衡。虽然语义化版本(SemVer)是目前的主流解决方案,但它存在局限性。未来,依赖管理可能需要更多依赖于测试和持续集成(CI)来验证变更的安全性,而不是仅仅依赖版本号。此外,依赖管理需要清晰的策略和责任分配,无论是作为依赖的提供者还是消费者。
精彩语录
-
“依赖管理的核心是处理依赖网络,而不仅仅是单个依赖。”
解释:依赖管理的复杂性在于处理整个依赖网络的变化,而不是孤立的单个依赖。 -
“SemVer是一个有损的兼容性估计。”
解释:版本号只能提供变更风险的估计,而不能完全预测依赖的实际兼容性。 -
“依赖管理需要考虑时间的影响。”
解释:随着时间推移,依赖的维护成本和变更风险会增加。 -
“依赖管理的成功在于避免冲突需求。”
解释:有效的依赖管理需要解决依赖网络中的版本冲突问题。 -
“依赖管理的最终目标是确保依赖的稳定性和安全性。”
解释:依赖管理不仅仅是技术问题,更是软件工程中的维护和策略问题。