2.14

查看英文版

2.14 项目与仓库结构

概述与动机

项目与仓库结构是代码库的物理组织方式:那些决定任何一样东西存放在何处的文件夹、文件和命名约定。仓库(repository,常简称为 “repo”)是持有一个项目的文件及其历史记录的版本控制容器。项目(project,当它将若干相关组件组合在一起时,有时也称为 solution)是你正在构建的那个软件的逻辑单元。结构就是你用来查找、理解和修改这个软件的地图。

在一个小团队里,一个人就能把整个布局记在脑子里。而在一个拥有成百上千名工程师、团队间频繁流动、承包商不断加入和离开的大团队中,每一个组织方式各不相同的仓库,都会收取一笔新的认知税。当你打开一个陌生的仓库时,你应该能够猜到源代码、测试、文档和部署配置分别存放在哪里,而无需阅读任何手册。当每个仓库都用同样的方式回答这些问题时,人员流动的成本就很低,入职也很快。而当每个仓库都是一片独一无二的雪花时,每一次切换上下文都会变成一次小型的研究项目。

在企业和政府环境中,一致的结构也是一项控制与保障方面的关切。审计人员、安全评审者,以及往往在原作者早已离开多年后才接手的长期维护者,需要能够可靠地找到规范文档、许可证文件、安全政策和构建定义。一个可预测的布局也能让自动化工具(扫描器、依赖分析器、合规检查)在整个系统组合中以同样的方式工作。因此本章把结构当作一项你只需决定一次、并在各处应用的约定来看待。它与编码标准与风格(第 2.1 章)、版本控制与源代码管理(第 2.6 章)以及文档(第 2.7 章)密切相关。

关键原则

  • 遵循最小意外原则:布局应符合一名经验丰富的工程师的预期,这样就无需死记硬背。
  • 跨仓库的一致性胜过局部的聪明设计;一个在各处都足够统一的结构,比在某一处臻于完美的结构更有价值。
  • README 是前门;新人应该仅凭它就能确定方向。
  • 通过命名让结构具备自解释性,使文件夹和文件能宣告自己的用途。
  • 用脚手架和模板来落实结构,而不是靠意志力和评审意见。
  • 在物理层面分离关注点:源代码、测试、文档、构建和部署应各自处于独立、可预测的位置。
  • 组织依赖关系,使其朝单一方向流动,从稳定的核心流向易变的边缘。

建议

采用一致的顶层布局

定义一套标准的顶层文件夹,让每个仓库在适用的情况下都使用它们,并记录每一个文件夹的用途。一种常见的、与厂商无关的约定包括:一个用于生产代码的源代码文件夹(通常为 src);一个用于自动化测试的测试文件夹(通常为 test 或 tests);一个用于文档的 docs 文件夹;一个用于构建定义和产出物的 build 文件夹;一个用于部署和基础设施即代码(服务器、网络和服务的机器可读定义,见第 8.2 章)的 deploy 文件夹;一个用于自动化和开发者工具的 scripts 文件夹;一个用于可运行示例的 examples 文件夹;以及一个用于需求和设计规范的 spec 或 specification 文件夹。并不是每个仓库都需要每一个文件夹,但只要某个关注点存在,它就应该以预期的名称存放在预期的位置。

让 README 成为入口

要求仓库根目录下必须有一个 README 文件,作为唯一的、规范的起点。它应该说明这个项目是什么、如何构建和运行它、如何运行测试、去哪里查找更深入的文档、由谁负责,以及如何参与贡献。README 并不是整套文档,它是指向其余部分的索引(第 2.7 章)。把缺失或过时的 README 当作一个缺陷来对待,因为它是每一位新工程师、审计人员或集成方最先阅读的东西。

标准化编辑器和配置文件

把共享的编辑器和工具配置纳入版本控制,让每一位贡献者都自动获得一致的行为。一个 .editorconfig 文件(一种简单的、与编辑器无关的文件,用于定义空白字符、缩进和换行规则)能让不同编辑器和操作系统之间的基本格式保持统一。为版本控制系统添加一个忽略文件(确保构建产出物和本地产物永远不会被提交),再加上第 2.1 章所述的共享格式化工具和代码检查工具配置。这些文件让仓库的约定变得真正生效,而不只是停留在文档里。

明确命名和文件夹约定

就文件夹和文件命名的约定(大小写、分隔符、单复数,以及诸如标记测试所用的必需后缀)达成一致,并统一应用。名称应该揭示意图,并与组织内其他地方使用的领域词汇保持一致。目标很简单:一个路径应该传达含义,让人只看文件夹或文件名就能知道里面有什么,而无需打开它。

有意识地组织层次和依赖关系

组织代码库,让其架构层次体现在文件夹布局中,并让依赖关系朝单一、合理的方向流动。高层的策略不应依赖于底层的细节。共享的、稳定的代码应该放在许多模块都能触及、又不会产生循环依赖的位置。当你把分层落实为物理结构、体现在目录树中时,工程师更有可能遵守它,违规行为也更容易在评审和自动化依赖检查中被发现。

用脚手架和模板落实结构

提供脚手架(scaffolding),即自动生成一个起始项目的机制,让新仓库从一开始就是正确的。一个模板或 cookiecutter(一种参数化的项目骨架,根据几个提示的回答生成一个现成的仓库)能把标准布局、README、配置文件和 CI 设置统一编码在一处。当工程师从一个共享模板创建新服务时,一致性就成了默认状态,而不是一种愿望,对模板的改进也会流向未来的项目。

在大规模场景下保持众多仓库的结构一致

把布局本身当作一项受治理的标准来对待:像任何其他工程标准一样进行集中维护(第 1.7 章),并像代码一样进行版本控制(第 2.6 章)。发布它,提供实现它的模板,只通过一套有文档记录的例外流程允许偏离,从而让”这项标准”保持其意义。在组合规模上,结构几乎全部的价值都来自它在各仓库间的统一性,因此漂移是需要管理的主要风险。

让结构指导单体仓库与多仓库的选择

把结构与第 2.6 章所述的仓库边界决策联系起来。单体仓库(monorepo,即一个仓库容纳多个项目)需要一套清晰的内部约定来分离各个项目及其共享代码,从而让这一整棵树保持可导航。多仓库(multi-repo)方式(许多小仓库,每个项目或服务一个)则需要强有力的跨仓库一致性,这样即便每个仓库独立存在,也能让人感到熟悉。无论哪种方式,一套有文档记录、模板化的结构才是保持导航可预测性的关键。边界选择改变的是你在何处应用这项约定,而不是你是否需要它。

权衡:优点与缺点

选择优点缺点
严格的组织级统一标准布局即刻熟悉;工程师可自由流动;工具统一对少数特殊项目适配不佳;需要治理
各团队自由选择布局局部优化;高度自主碎片化;上下文切换成本高;工具不一致
脚手架和模板仓库默认即正确;改动可传播需要维护模板;生成出的仓库存在漂移风险
深层、分层的文件夹层级结构清晰明确;边界清楚导航开销大;路径过长;有过度设计的风险
扁平、浅层的布局易于浏览;仪式感低分离不充分;随着项目增长而失效

主要的权衡在于统一性与自主性之间。一套单一的标准布局,能为大多数在不同代码库之间流动的工程师消除摩擦,代价是偶尔会有个别项目的需求与这套模板不太契合。在一个大型组织中,来自熟悉度的集体收益几乎总是超过这种局部损失。这正是为什么推荐的姿态是一套强有力的默认标准,加上一条有文档记录的例外路径(第 1.7 章),而不是僵化的统一,也不是无管理的自由。一个次要的权衡是深度与简洁之间:结构应足以分离真正的关注点,但又不能多到让导航变成一场穿越空文件夹的跋涉。

与团队讨论的问题

  1. 当一名工程师转到我们一个陌生的仓库时,他们需要多久才能找到测试、部署配置和负责人? 这就是结构存在的意义所在()消除的那种导航税,而在组合规模上,这笔税每年会以小额的形式被支付成千上万次,累积成严重的工程时间损失。最小意外原则的意义在于,一名经验丰富的工程师应该无需阅读手册就能猜到源代码、测试、文档和部署分别存放在哪里,所以诚实的检验标准是这种猜测在你们的仓库中是否真的成立。带着一个真实的数字来开会:给自己计时,看看在两三个陌生的内部仓库中完成定位需要多久,或者调取新人做出第一次改动所需时间的入职数据。如果答案要以数天的研究来衡量,而不是数分钟的辨认,那你就量化出了雪花仓库的代价,这也证明了投资一套所有仓库共享的标准布局是值得的一次性投入。

  2. 我们的架构层次是否体现在文件夹树中,还是依赖循环隐藏在一个扁平布局里? 结构的意义不仅仅在于可查找性:当你把分层落实为物理结构时,工程师会尊重它,评审者和自动化依赖检查也能发现违规;而一堆扁平的文件则会让不当的耦合和循环悄悄潜入,直到改动变得危险。在一个庞大、长期存续的系统中,这正是防止高层策略悄悄依赖底层细节的关键所在,这也正是那种预防成本低、事后清理成本高的侵蚀。拿出你的依赖关系图,或者做一次快速检查:是否存在循环,是否有稳定的东西依赖于不稳定的东西?答案应该促使你把层次体现在目录中,并增加自动化的依赖方向检查,从而让边界在目录树中可见、在流水线中被强制执行,而不是只存在于某个人的脑海模型里。

  3. 我们的新仓库是从模板开始就正确的,还是依赖一份维基页面和良好的意愿? 由脚手架强制落实的结构是默认状态;由文档描述的结构会漂移,因为现实遵循的是生成仓库的东西,而不是页面上说它们应该长什么样。对于大型或受监管的组织而言,这也是一项保障方面的关切:当每个仓库都由一个共享模板生成时,安全扫描器、依赖分析器和审计人员每次都能在同一个地方找到许可证、安全政策、规范和构建定义,无论跨越多少供应商、多少年份。带上证据:你最近的仓库中有多少是从标准模板脚手架生成的,有多少是手工拼装的,而那些模板化生成的仓库后来又漂移了多远?应采取的行动是,让模板成为创建仓库的唯一简便方式,把它当作一项受版本控制的标准来治理,并配以有文档记录的例外路径,同时自动检测漂移,因为结构几乎全部的价值都存在于统一性之中。

  4. 我们是否已经决定了标准是覆盖一个单体仓库还是多个独立仓库,同一套约定在这条边界的两侧是否真的都成立? 仓库边界的选择改变的是你在何处应用这项约定,而不是你是否需要它,一旦选错,要么是一棵谁都无法导航的巨型大树,要么是一堆彼此陌生的散乱仓库。单体仓库需要一套清晰的内部约定来分离各个项目及其共享代码,从而让这一整棵树保持可导航;多仓库方式则需要强有力的跨仓库一致性,让每个独立的仓库依然让人感到熟悉。带上当前的清单:你们有多少个仓库、任何单体仓库内部的共享代码是如何分离的,以及一次计时测试()一名工程师能否像在独立仓库中一样,同样快地在大树中找到一个项目。对于一个由不同供应商各自交付独立仓库的大型企业或政府项目而言,要有意识地决定这套约定中哪些部分是普适的、哪些是与边界相关的,因为无论代码是以一棵树还是五十棵树的形式送达,审计人员和平台工具都必须以同样的方式工作。

  5. 谁负责我们的结构标准,当一个项目真的不适合它时,实际会发生什么? 在组合规模上,结构几乎全部的价值都来自统一性,所以真正的风险在于一个无人负责、逐渐腐坏的标准,以及一条模糊到每个团队都在悄悄发明自己布局的例外路径。这里的张力存在于不适合任何特殊项目的僵化统一,与让一切都碎片化的无管理自由之间,健康的答案是一套强有力的默认标准,加上一条由指定负责人治理、像代码一样版本化、有文档记录、可审计的例外流程。带上证据:是否有一名唯一的责任负责人、一份带变更日志的版本化标准文档、一份记录已批准例外及其原因的日志,以及一个你能在现实中找到的、未经记录的偏差数量统计。在企业和政府环境中,一个无人记录的例外就是一个控制缺口,因此要把每一次偏差都与一份书面理由和一个复审日期关联起来,并确保强制要求该布局的采购合同也明确指定谁有权批准偏离它。

  6. 我们的 README 和已纳入版本控制的配置文件,是让我们的约定真正生效,还是仅仅是装饰? README 是前门,而已纳入版本控制的 .editorconfig、忽略文件和代码检查工具配置,是让约定自我强制执行的关键,然而这些恰恰是最先过时、也最少有人注意的东西,直到审计人员或新人无法让项目构建起来。这里的张力存在于一份保持最新的精简 README 与一份内容详尽却容易漂移的 README 之间,也存在于信任人们正确地格式化代码,与让共享配置自动强制执行之间。带上一个样本:抽取五个仓库,检查有多少 README 真正说明了这个项目是什么、如何构建、测试和运行它,以及由谁负责,又有多少携带了共享配置文件,而不是依赖个人习惯。对于一个大型或受监管的组织而言,由于集成方、安全评审者和长期维护者会在任何其他事情之前先阅读 README,要把缺失或过时的前门当作一个有责任人的缺陷来对待,并自动检查配置文件是否存在,这样合规就不必依赖善意。

分行业视角

初创公司。 速度制胜,所以为你的第一个仓库约定一套简单、足够扁平的布局(src、test、docs、scripts、一份内容充实的 README、一个 .editorconfig 和忽略文件),并在同一个下午把它保存为一个轻量级模板。用它生成第二个服务,这样两个仓库都让人感到熟悉,一名新来的承包商能在几个小时内完成入职,而不必逆向工程一片雪花。抵制你目前还不需要的深层层级和繁重治理;这里全部的回报就是两位创始人和一名承包商共用一张地图。

小型企业。 没有平台专家、预算又紧张时,采用你的语言或框架已经预设的传统布局,而不要自己发明一套,这样现成的工具和任何新员工到来时就已经对它有所了解。购买脚手架(一个框架生成器或一个 cookiecutter 模板),而不是自己构建;把你稀缺的精力花在维护一份内容充实、保持最新的 README 上。这份 README 是你能买到的最便宜的保险,以应对那个唯一知道布局的人离开的那一天。

企业。 在众多团队和成百上千个仓库中,目标是统一性:发布一套版本化的结构标准,从共享模板生成每一个新服务,自动检测漂移,只通过一套有文档记录的例外流程允许偏离。由于每个仓库看起来都一样,一名被重新分配到新团队的工程师能在几小时内就有产出,覆盖整个系统组合的安全和依赖扫描器也能每次都在同一个地方找到许可证、安全政策和构建定义。要明确为模板维护和漂移检测编列预算,因为这项持续维护正是让标准在规模化场景下保持意义的关键。

政府。 采购、透明度和长期问责塑造着这种布局,因此要在约束每一个供应商的交付标准中强制要求统一的结构。要求一个把代码与已批准需求关联起来的 specification 文件夹、根目录下的许可证和安全政策文件,以及一个存放基础设施即代码定义的 deploy 文件夹,从而让审计人员在每一个系统中都以同样的方式定位合规证据。由于来自不同供应商的承包商都遵循同一张地图,合同结束后的维护成本会低得多,公众也能获得一条从需求到可运行代码、经得起推敲、可供检查的轨迹。

示例

初创公司。 一家三人规模的初创公司为其第一个仓库约定了一套简单的标准布局(src、test、docs、scripts、一份内容充实的 README、一个 .editorconfig 和忽略文件),并将其保存为一个轻量级模板。一个月后,当他们启动第二个服务时,他们从这个模板生成它,因此两个仓库都已经让人感到熟悉,那位新来的承包商在一个下午内就完成了入职。他们抵制了目前还不需要的深层文件夹层级,让这棵树保持足够扁平,可以一眼扫过。代价只是一个下午的搭建工作,却让他们免于那种会让未来每一个仓库都变成一个小型研究项目的雪花式蔓延。

企业。 一家跨国零售商在多种语言中运行着数百个服务。其平台团队发布了一套版本化的仓库结构标准,以及一组实现它的项目模板。每个新服务都从一个模板生成,因此它一诞生就带有标准的 src、test、docs、deploy 和 scripts 文件夹、一份内容充实的 README、一个 .editorconfig、忽略文件,以及一条可用的 CI 流水线。由于每个仓库看起来都一样,一名被重新分配到新团队的工程师能在几小时内就有产出,组织范围内的安全和依赖扫描器也能统一运行,因为它们总能在预期的地方找到文件。

政府。 一家正在对遗留系统进行现代化改造的国家机构,在其面向所有供应商的交付标准中强制要求统一的仓库布局。每个仓库都必须包含一个把代码与已批准需求关联起来的 specification 文件夹、一份有文档记录的 README、根目录下的许可证和安全政策文件,以及一个存放基础设施即代码定义(第 8.2 章)的 deploy 文件夹。由于来自不同供应商的承包商都遵循同一套结构,该机构的审计人员能在每一个系统中以同样的方式定位合规证据,合同结束后的长期维护成本也低得多,因为接手的维护者已经了解这张地图。

商业论证:动机、投资回报率与总拥有成本

采用一套结构标准的成本大多是一次性的:就布局达成一致、构建模板、记录约定文档。持续成本很低,主要集中在维护模板和治理例外上。而没有标准的成本则是持续的、复合累积的:每一位打开陌生仓库的工程师都要缴纳一笔导航税,每一次入职都会变慢,自动化工具也必须逐仓库配置,因为没有任何东西出现在你预期的位置。在一个大型组织中,这些微小的摩擦会累积成严重的工程时间损失。

回报体现为更快的入职速度、更低的团队间流动成本、来自组合范围工具的更高信号质量,以及在受监管环境中更低的审计和长期维护成本,因为产物总能被找到。总拥有成本(TCO,即构建、运行和维护一个系统的全部生命周期成本)在长期存续的系统中下降得最多,因为受益于可预测结构的维护者,往往并不是当初创建它的作者。要向领导层论证这一点,把结构定位为一项低成本、高杠杆的标准,能提升开发者生产力和审计就绪度,并用入职时间数据和在陌生仓库中翻找东西所耗费的精力,为今天不一致所付出的代价量化一个数字。

反模式与陷阱

  • 雪花仓库: 每个仓库的组织方式各不相同,导致每一个都必须从头重新学习。
  • 缺失或过时的 README: 没有前门,迫使新人逆向工程出如何构建和运行这个项目。
  • 靠文档而非模板来实现结构: 一份维基页面描述了标准布局,但没有任何东西生成或强制执行它,导致现实逐渐偏离它。
  • 模板漂移: 从一个模板生成的仓库随着时间推移而分化,对模板的改进永远无法到达它们。
  • 过度设计的层级: 深深嵌套的近乎空的文件夹,增加了仪式感却无助于导航。
  • 关注点混杂: 源代码、测试、构建产出物和密钥杂乱地混在一起,没有清晰的分离。
  • 提交了构建产出物和本地产物: 由于从未设置忽略规则,生成的文件被纳入了版本控制,污染了历史记录和差异对比。
  • 被扁平结构隐藏的分层违规: 没有物理边界,导致依赖循环和不当耦合悄悄潜入,无人察觉。

成熟度模型

  • 第 1 级(启动): 每个仓库都由其作者临时起意地组织,随需应变;布局差异很大;README 缺失或不可靠;新人必须被人手把手地带着走过每一个仓库。
  • 第 2 级(发展): 存在一些非正式的基本约定,许多仓库彼此相似;一些团队维护着自己的起始布局;但没有权威标准,没有共享脚手架,结构从一个团队到另一个团队会出现明显漂移。
  • 第 3 级(标准化): 一套有文档记录、版本化的结构标准在整个组织范围内被强制执行;新仓库从携带标准布局、README、配置文件和 CI 的共享模板生成;偏差要经过一套有文档记录的例外流程,而不是悄悄发生。
  • 第 4 级(管理): 对标准的符合度被度量并以数据加以控制:自动化检查会报告有多大比例的仓库符合布局、模板化生成的仓库漂移了多远、README 的完整度,以及依赖方向的违规情况,这些都对照基线进行跟踪;入职和导航时间被测量;例外被记录并接受复审,模板变更基于证据而非意见得到批准。
  • 第 5 级(编排): 结构持续改进并自适应:模板的改进会自动传播到现有仓库,结构治理与安全、合规和平台工具相集成,标准随着语言、架构和系统组合的变化而有意识地演进,在组织周围环境不断变化的同时保持高度的统一性。

讨论思路

  • 在你的组织中,哪些顶层文件夹应该是真正普适的,哪些应该是可选的?
  • 你如何防止从模板生成的仓库随时间推移而逐渐偏离它?
  • 有帮助的分层层级与过度设计的文件夹仪式感之间的界线在哪里?
  • 你的结构标准在单体仓库和多仓库方式之间应该有多大区别(如果有的话)?
  • 对于一个真实需求不符合标准布局的项目,怎样的例外流程才是恰当的?
  • 你的结构中有多少能被自动检查,又有多少仍然依赖人工评审?
  • 谁负责结构标准及其模板,变更是如何被提出和推行的?

关键要点

  • 组织每一个仓库,使任何工程师都能凭预期在任何代码库中导航,遵循最小意外原则。
  • 采用一致的顶层布局(源代码、测试、文档、构建、部署、脚本、示例、规范),并让 README 成为入口。
  • 把编辑器和工具配置(如 .editorconfig)纳入版本控制,让约定真正生效,而不仅仅是写在文档里。
  • 用脚手架和模板落实结构,让新仓库默认就是正确的。
  • 在规模化场景下,价值在于统一性:治理这项标准,管理漂移,只通过有文档记录的例外允许偏差。

参考资料与延伸阅读

  • Robert C. Martin, Clean Architecture: A Craftsman’s Guide to Software Structure and Design
  • Steve McConnell, Code Complete: A Practical Handbook of Software Construction
  • Andrew Hunt and David Thomas, The Pragmatic Programmer
  • Titus Winters, Tom Manshreck, and Hyrum Wright (eds.), Software Engineering at Google
  • Scott Chacon and Ben Straub, Pro Git
  • EditorConfig project documentation (as a reference standard for editor configuration)