什么是真正的单体代码库?
在软件公司中,经常会有关于是否应该采用“单体代码库”的讨论,即“公司所有代码都存放在一个版本控制的仓库中”。很多人做这个决定是基于谷歌存储代码的方式。
作者曾在拥有先进单体代码库的公司(谷歌)和拥有先进多仓库系统的公司(LinkedIn)的开发者生产力部门工作过。必须指出的是:人们通常认为单体代码库所具有的许多有价值特性,实际上与源代码控制仓库的数量无关。
事实上,人们(以及谷歌)所认为的单体代码库实际上包含多个不同的概念:
- 跨项目的原子提交(因此存在一个原子的“头”提交,所有代码都原子性地向前推进)
- 统一的目录层次结构和所有源代码的单一视图
- 检出或提交代码的单一位置(包括所有读写内容的工具)
- (有时)检出、提交和依赖的最小单位是文件
- (通常)没有项目的概念,只有目录和文件的概念
- (有时)单版本规则:任何时候仓库中任何依赖项只能存在一个版本
- 要求库维护者解决他们引起的问题的能力
下面将更详细地讨论这些概念,包括它们的优缺点。
跨项目的原子提交
假设有两个独立的项目A和B。需要做一个同时影响这两个项目的更改。单体代码库的部分特性是保证可以同时原子性地提交到这两个项目。不存在项目A处于提交#1而项目B处于提交#2的仓库视图。
这在需要同时更改项目A和B才能避免破坏的情况下尤其重要。例如,假设有一个名为App的项目,它依赖于一个名为Library的项目。想要更改Library中函数的签名,同时更新App。如果只更新Library或只更新App,那么App就会被破坏。
这个特性最依赖于将所有内容放在单个源代码仓库中,因为“仓库”的实际定义是“可以原子性地提交多个文件的位置,它跟踪这些原子提交,并且可以在原子提交历史中的任何点检出”。
这个特性还意味着整个仓库有单一的“头”(最新提交)定义。这一点很重要,因为当开发者从仓库检出时,他们通常检出的是“头”。这意味着当开发者检出时,无论同时检出多少项目,都能保证获得整个源代码树的一致视图。他们永远不必担心是否检出了不兼容版本。
标准化的跨项目目录结构
单体代码库中的所有代码都被认为位于单个目录结构中。这在开发和浏览代码时都有优势。
开发期间:检出标准化
在开发期间,如果项目A存储在仓库的/path/to/project/A
,项目B存储在/path/to/project/B
,那么当同时检出它们时,它们将位于相邻的目录中。可以保证目录结构始终一致。如果需要在开发期间让它们协同工作,永远不必考虑应该将项目A放在磁盘上的什么位置相对于项目B。
统一的代码浏览方式 由于有单一的目录结构,在代码搜索工具中浏览目录相对简单,并且可以使用单一的代码搜索工具搜索该仓库。
单一的检出和提交位置
单体代码库的价值之一是不必思考“应该从哪个仓库检出?”开发者只需要考虑需要检出什么代码。类似地,所有提交都进入同一个仓库。
这也意味着可以查看历史中所有提交的单一视图,这在某些情况下很有帮助(例如为了调试目的,试图找出时间A和时间B之间可能发生的一切变化)。
最后,所有工具只需要关心访问单个仓库——它们只需要关心目录和文件名。
文件是检出、提交和依赖的最小单位
在大多数单体代码库中,可以提交的最小单位(由版本系统跟踪)是文件。系统知道“文件”是发生变化的内容。
在某些单体代码库中,还可以在不检出整个仓库的情况下检出单个文件。实际上,如果仓库变得非常大,这就成为一个非常重要的生产力特性。
此外,在某些单体代码库中(特别是谷歌的),依赖的最小单位是文件。这意味着构建系统可以知道一个文件依赖于另一个文件。
没有项目的概念
由于所有内容都在同一个仓库中,没有固有的概念表明不同的目录集合可以代表单个“项目”。构建系统可能知道某些目录被编译在一起以产生特定的工件,但没有通用的方法可以通过查看目录结构等来轻松看到这一点。
相比之下,多仓库系统可以说每个仓库都是一个项目,这将提供一个更具体的工件来表示项目。
单版本规则
通常,单体代码库会强制要求任何给定软件在仓库中同时只能存在一个版本。如果签入库,在整个仓库中只能签入该库的一个版本。由于拥有单体代码库,这意味着该公司在任何给定时间只能存在该库的一个版本。
这样做有多个原因。
首先,它使得推理系统行为更加容易。总是理解将获得哪个版本的依赖项。不必每次检出一段代码时都检查传递依赖树来理解实际获得的内容,因为获得的是检出时仓库中存在的该依赖项的版本。
但这样做最重要的原因是,大多数编程语言强制要求最终程序中任何特定依赖项只能存在一个版本。
这个规则有一些相当显著的缺点。如果拥有很多人依赖的代码,升级该代码可能非常困难,因为所做的任何更改都会破坏某些人。
当涉及第三方库时,这个问题变得更加严重。如果所有代码都必须存在于仓库中,这意味着必须将第三方库签入仓库。而且对于公司中的每个人来说,只能存在一个版本。
让库维护者解决他们引起的问题
在单体代码库的世界中,如果拥有一个库,可以通过签入与依赖项目不兼容的内容来破坏每个依赖项目的构建。在单版本世界中尤其如此,库所有者必须签入每个人都依赖的库的单一版本。
这主要是公司政策的问题,但在可以实际执行的世界中更容易做到,并且有某种系统在库开发者给他人带来痛苦时也会让他们感到痛苦。
总结
可以看到,“单体代码库”实际上远不止是将所有内容放在一个源代码仓库中。有些人将所有这些内容组合在一起,因为以上基本上是对谷歌单体代码库的描述,大多数人在谈论“单体代码库”时似乎都在考虑那个系统。但分离这些概念很重要,因为其中许多可以在现有系统中实现。此外,也许并非所有这些实际上都是好的,应该有意地选择要在业务中采用哪些。