译:基础的东西

原文:https://matklad.github.io/2024/03/22/basic-things.html
作者:matklad
译者:ChatGPT 4 Turbo

编者注:虽然做开源多年,但有人把这些事情都细致地整理出来,看过一遍后还是收获颇丰的。

在参与了几个较大项目的初期阶段工作后,我积累了一份清单,这些项目共享以下三个特性:

  • 当项目规模小的时候,它们是无关紧要的,
  • 当项目规模大的时候,它们能够大幅提高生产力,
  • 在项目后期引入它们会更加困难。

以下是清单:

README

一个项目应该有一个 简短的 单页自述文件,主要是链接到更多主题性的文档。两个最重要的链接是用户文档和开发者文档。

一个常见的失败是自述文件通过累积而杂乱无章地增长,使其既不是一个好的 Landing 页面,也不是任何特定主题的全面文档来源。之后重构这样无结构的自述文件很难。尽管信息是有价值的,如果没有组织,但没有更好的地方可以转移。

开发者文档

对于开发者来说,你通常希望在仓库中有一个文档文件夹。文档文件夹还应该包含一个简短的首页,描述文档的结构。这个结构应该允许少量高质量的精选文档,和大量关于任何特定主题的临时增补式笔记。例如,docs/README.md 可以指向精心制作的 ARCHITECTURE.mdCONTRIBUTING.md,它们描述了高层次的代码和社会架构,并明确说明 docs/ 文件夹中的其他内容都是一组无组织的主题指南。

这里的常见失败模式有:

  1. 根本没有地方可以添加新的开发者文档。因此,没有文档被编写,而到了需要文档的时候,知识已经丢失。
  2. 只有高度结构化、仔细审查的开发者文档。贡献文档需要很多努力,许多小事情没有被记录下来。
  3. 只有无结构的、只增不减的孤立文档堆。事情 大部分 都有记录,往往不止一两次,但任何新团队成员都需要重新做筛选。

用户网站

大多数项目都可以从一个专门针对用户的网站中受益。当用户寥寥无几时,你就希望网站已经准备就绪:使用量会随着时间的推移而累积,所以,如果你发现自己拥有大量用户却没有网络“面孔”,你已经损失了相当多的价值!

这里还有一些其他的失败模式:

  1. 不同的团队管理网站。这阻止了项目开发者直接贡献改进,可能会导致文档和已发布产品之间的分歧。
  2. 当今的网络技术栈倾向于无限复杂。在开始时选择一个“容易”的重型框架,然后让自己陷入 npm 的泥潭,这是太自然不过的了。网站是关于内容的,而内容具有引力。你在开始时选择的任何标记语言方言都会伴随你一段时间。因此,请谨慎考虑你的网络技术栈的选择。
  3. 谈论尚未完成的事情。不要过度承诺,稍后说更多的东西比收回你的话要容易得多,而且谦逊可能是一种不错的市场营销方式。考虑一下你是否处在一个工程信誉比流行词更快传播的领域。但这是情境性的。更通用的建议是,市场营销也会随着时间的推移而累积,因此从一开始就慎重塑造你的形象是值得的。

内部网站

这更具情境性,但考虑一下,除了面向公众的网站外,你是否还需要一个面向内部、面向工程的网站。在某个时刻,你可能需要比 README.md 中可用的更多交互性——也许你需要一个地方来展示与代码相关的指标,如覆盖率,或一些用于计算发布轮换的 javascript。拥有一个贡献者可以在没有太多繁文缛节的情况下立即放置他们需要的东西的网络空间是很好的!

这是一个反复出现的主题——你应该有组织,你不应该有组织。_一些_事情有广泛的影响,应该通过仔细审查来保护。_其他_事情则从简单存在和轻量级过程中受益。你需要为这两种事情创建空间,并且有一个关于什么放在哪里的明确决策规则。

对于内部网站,你可能还需要某种数据存储。如果你想跟踪提交之间的二进制大小,某个东西 需要将提交哈希映射到(乐观地说)千字节!这里我不知道好的解决方案。我在 github 仓库中使用 JSON 文件来处理类似的目的。

过程文档

有很多可能的方式将一些代码合并到主分支。选一个,并在 .md 文件中明确写出来:

  • 是将功能分支推送到中央仓库,还是每个人都在他们的分叉上工作?我发现分叉通常情况下工作得更好,因为它们自动为每个人的分支命名空间,并将团队成员和外部贡献者置于同等地位。
  • 如果仓库是共享的,分支的命名约定是什么?我将我的前缀设为 matklad/
  • 你使用非火箭科学规则(稍后详述 :)。
  • 谁应该对特定的 PR 进行代码审查?一个人,以避免旁观者效应并减少通知疲劳。审查者由 PR 的作者选定,因为这在高信任团队中是一个稳定的平衡,而且减少了红色胶带。
  • 审查者如何知道他们需要审查代码?在 GitHub 上,你希望_分派_而不是_请求_审查。分派是电平触发的 —— 直到 PR 被合并之前,它不会消失,并且成为审查者的责任,帮助 PR 推进直到它被合并(请求审查 仍然有用,以便在一轮反馈和更改后提醒被分派者)。更一般地说,代码审查是最高优先级任务 —— 如果已经有一些完成的代码仅仅因为等待你的审查而被阻塞,就没有理由去工作在新代码上。
  • 审查的目的是什么?审查正确性、统一性、习语、知识共享、高层架构是选择!在你的项目的上下文中明确阐述哪种最有意义。
  • 元过程文档:积极鼓励贡献过程文档本身。

风格

谈到元过程,风格指南在哪里它是最具实际价值的。确保在代码审查期间的大多数风格评论立即在项目特定的风格文件中得到体现。新贡献者应该通过十几个链接到风格指南的特定项目来学习项目的声音,而不是通过 PR 上的一百个重复评论。

你甚至需要一个项目特定的风格指南吗?我认为你需要 —— 减少对琐碎决定的心理能量是有帮助的。如果你需要一个结果变量,一半的函数称之为 res,另一半的函数称之为 result,做出这个选择只是分散注意力。

项目特定的命名约定是风格指南中放置的更有用的事情之一。

优化风格指南以便于扩展。将代码审查中的评论提升到风格指南中不应该需要太多工作。

确保有一个风格沙皇 —— 在 具体 风格选择周围建立共识是非常困难的,最好将整个责任委托给一个能做出足够好的选择的人。风格通常不是关于什么更好,它是关于以半随意的方式去除不必要的选项。

Git

记录关于 git 的风格细节。如果项目使用 area: 前缀进行提交,请列出这样的前缀的明确列表。

考虑记录摘要行的可接受行长度。Git 手册大胆地宣称摘要应该在 50 个字符以下,但这纯粹是错误的。即使在内核中,大多数摘要也在 50 到 80 个字符之间。

绝对明确禁止添加大文件到 git。仓库大小单调增加,git clone 时间很重要。

记录合并与变基的事项。我偏好的答案是:

  • 一个变更的单位是一个拉取请求,它可能包含几个提交
  • 拉取请求的合并提交是正在被测试的内容
  • 主分支只包含合并提交
  • 相反,只有 主分支包含合并提交,拉取请求本身总是被变基的。

禁止在仓库中使用大文件是一个好政策,但遵守起来很难。在项目的生命周期中,总会有人偷偷添加并撤销一个兆字节的生成的 protobufs,这将在代码审查的雷达下飞行。

这就引出了最基本的事情:

不是火箭科学的规则

始终保持一套定义明确的自动化检查,这套检查在主分支上始终通过。如果你不希望 Git 仓库中有大型文件块,编写一个拒绝大型 Git 对象的测试,并在更新主分支之前运行该测试。不想要功能分支上的合并提交?编写一个测试,如果检测到一个合并提交,就以一页 Git 自助信息的方式失败。想要将 .md 文件的宽度限制在 80 列内?编写一个测试 🙂

值得你重新阅读原始帖子:https://graydon2.dreamwidth.org/1597.html

这种关于代码库的属性单调增长的心态难以置信地 强大。你开始将代码视为临时的、流动的事物,它总是可以相对便宜地改变,而积累的自动化测试集合才是项目的真正价值。

另一个二阶效应是,NRSR 对优化构建和测试基础设施施加了压力。如果你没有选项在无关的易失败测试失败时合并代码,你就不会有易失败的测试。

这里常见的反模式是,一个项目逐渐增长了一组半检查 —— 存在的测试,但不是 100% 可靠,因此不被 CI 常规执行。这就产生了模糊性 —— 测试失败是因为回归需要修复,还是它们从未可靠过,只是测试了一个对项目的功能实际上并不重要的属性?这种模糊性随着时间的推移而加剧。如果一个检查不足够可靠,不能成为 NRSR CI 闸门的一部分,那它实际上就不是你关心的检查,应该被移除。

但要做到 NRSR,你首先需要构建 & CI 你的代码:

构建与持续集成

这是一个复杂的主题。让我们从基础开始:什么是构建系统?我想在这里强调一些稍微非传统的答案。

首先,构建系统是一个引导过程:它是你如何从 git clone 到一个可工作的二进制文件的过程。这个引导过程的两个方面非常重要:

  • 它应该是简单的。不需要 sudo apt-get install bazzilion packages,你的构建系统的单个二进制文件应该能够自动带来所需的一切。
  • 它应该是可重复的。你的笔记本电脑和你的持续集成(CI)应该以完全相同的依赖集结束。最终结果应该是提交哈希的函数,而不是你的本地 shell 历史,否则 NRSR 就无法工作。

其次,构建系统是开发者的用户界面(UI)。为了做几乎任何事情,你需要在你的 shell 中键入某种形式的构建系统调用。应该有一个单一的、明确记录的命令来构建和测试项目。如果它不是一个单一的 makebelieve test,那么就出问题了。

这里的一个反模式是当构建系统溢出到持续集成时。当你需要阅读 .github/workflows/*.yml 来编译命令列表以了解检查集合内容时。这是意外的复杂性!蔓延的 yamls 是一个糟糕的入口点。将所有逻辑放入构建系统并让持续集成驱动它,而不是反过来。

这里有一个更强的建议。无论项目的大小如何,对它而言可能只有一小部分工作流程是有意义的:测试、运行、发布等。这一小组工作流程应该从一开始就确定下来,并且应该记录具体的命令。当项目随后增长时,这组构建系统的入口点 不应该 增长。

如果你添加了一个 Frobnicator,makebelieve test 调用 应该 测试 Frobnicator 是否有效。如果相反,你需要一个专门的 makebelieve test-frobnicator 和某个 CI yaml 中的对应行,你就走上了危险的道路。

最终,构建系统是一系列命令的集合,用以实现某些操作。在大型项目中,你不可避免地需要一些非平凡量的胶合自动化。即使入口仅仅是 makebelive release,内部可能需要许多不同的工具来构建、签名、标记、上传、验证,并为新版本生成变更日志。

一个常见的反模式是用 bash 和 Python 编写这类自动化,但这几乎完全是技术债务。这些生态系统本身非常挑剔,而且,至关重要的是(除非你的项目本身是用 bash 或 Python 编写的),它们是你项目中已有的“常规”代码的第二个生态系统。

但发布软件也只是代码,你可以用你的主要语言来编写。合适的工具往往是你已经在使用的工具。从一开始就明确地解决胶合问题并选择/编写一个库以便易于编写子进程处理逻辑是值得的。

总结构建和 CI(持续集成)的故事:

构建系统是自包含的、可复现的,并且承担下载所有外部依赖项的任务。不论项目大小,它包含 O(1) 个不同的入口点。CI 基础设施触发这些入口点之一运行一套标准检查,这不是什么高深的科学规则。对自由形式自动化有明确的支持,这种自动化用的是与项目大部分相同的语言实现。

与 NRSR(不是火箭科学规则)的集成是构建过程中最重要的方面,因为它决定了项目随时间的演进方式。让我们放大看看。

测试

测试是一个主要的架构关注点。当编写第一行代码时,你就应该理解大局的测试故事。重点绝对_不是_“每个类和模块都有单元测试”。测试应当是数据导向的——特定软件的工作是接受一些数据,转换它,并输出不同的数据。整体测试策略需要:

  • 指定 / 生成输入数据的某种方式,
  • 断言输出数据所需属性的某种方式,以及
  • 快速运行许多单独检查的方式。

如果时间是输入数据的重要部分,应该明确地对其进行建模。通常,测试架构设计不当会导致:

  • 软件难以更改,因为成千上万的测试钉牢了现有的内部 API。
  • 软件难以更改,因为没有测试来自信地验证无意中的破坏缺席。
  • 软件难以更改,因为每次变更都需要几小时的测试时间来验证。

如何架构测试套件超出了本文的范围,但请阅读单元和集成测试如何测试

本文范围内的一些具体事项:

对于不稳定的测试零容忍。严格的非火箭科学规则通过构建给出了这一点 —— 如果你因为别人的测试不稳定而无法合并_你的_拉请求,那么该不稳定的测试立即成为你的问题。

快速测试。同样,NRSR 已经自然地为此提供了压力,但另外,使测试时间更加显著同样有所帮助。仅仅默认打印总测试时间和运行中五个最慢的测试就已经有很大帮助了。

并非所有测试都能快速完成。延续拥抱秩序和混沌的阴阳主题,尽早引入慢测试的概念是有帮助的。CI 总是运行完整套件的测试,快速和慢速。但本地的 makebelive test 默认只运行快速测试,慢速测试可以选择加入。加入可以像设置一个 SLOW_TESTS=1 环境变量那样简单。

尽早引入快照测试 库。尽管大部分测试可能应该使用项目特定的测试工具,但对于其他所有东西,内联 repl 驱动的快照测试是一个好的默认方法,一旦你积累了一批非基于快照的测试,引入它会非常昂贵。

与测试并行的,是基准测试。

基准测试

我并没有一个关于如何在一个大型、活跃的项目中使基准测试工作的宏伟愿景,这对我来说总感觉是一场挣扎。不过,我确实有一些战术性的提示。

首先,任何在 NRSR 期间没有运行的代码实际上都是死代码。在性能改进旁边添加基准测试,然后不将它们接入 CI 是非常常见的情况。所以,两个月后,基准测试要么彻底停止编译,要么可能因为某些无关的变更在启动时就 panic。

这里的解决方案是确保每个基准测试也是一个测试。按输入大小参数化每个基准测试,使得输入小的情况下它能在毫秒内完成。然后编写一个测试,它实际上只是用这个小输入调用基准测试代码。并记住,你的构建系统应该有 O(1) 的入口点。将这个接入 makebelieve test,而不是一个专用的 makebelieve benchmark --small-size

其次,任何大型项目都有一定数量的非常重要的宏观指标。

  • 构建需要多长时间?
  • 测试需要多长时间?
  • 发送给用户的结果产物有多大?

这些都是始终重要的问题。你需要基础设施来跟踪这些数字,并定期查看它们。这就是内部网站及其数据存储发挥作用的地方。在 CI 期间,记录这些数字。CI 运行结束后,上传一个包含提交哈希、指标名称、指标值的记录到_某处_。如果结果嘈杂也不要担心 — 你的目标是这里的基线,能够随时注意到随时间发生的大变化。

“上传”部分有两个选项:

  • 只需将它们放入 git 仓库中的某个 .json 文件,并利用一点点 javascript 来显示这些数据的漂亮图表。
  • https://nyrkio.com 是一个我可以推荐的出奇好用的 SaaS 服务。

模糊测试

认真的模糊测试奇怪地兼具了测试和基准测试的特点。像普通测试一样,模糊测试通知你应用中的一个正确性问题,并且是可复现的。像基凊测试一样,它是(无限)长时间运行的,在 NRSR 中进行是不切实际的。

我还没有掌握如何最有效地将持续的模糊测试整合到开发流程中。我不知道什么是模糊测试的不是火箭科学的规则。但有两件事有所帮助:

首先,即使你不能在 CI 中运行模糊测试循环,你也可以运行孤立的种子。为了确保模糊测试代码不会被破坏,做同样的事情,就像与基准测试一样 —— 添加一个测试,用固定的种子和小的、快速的参数运行模糊测试逻辑。这里的一个变化是,你可以使用提交 sha 作为随机种子 —— 这样代码仍然是可重现的,但有足够的变化避免动态死代码。

其次,从级触发的角度思考模糊测试是有帮助的。有了测试,当你提交一个错误的提交时,你会立即知道它破坏了东西。使用模糊测试,你通常稍后发现这一点,而且一个破坏了的种子通常会持续好几个提交。所以,作为模糊测试器的输出,我认为你想要的 不是 一组 GitHub 问题,而是某种仪表板,显示近期提交和这些提交的失败种子的表格。

有了这个不是火箭科学的规则作为坚实的基础,考虑发布就变得有意义了。

发布

这里有两个核心洞见:

首先,发布 流程 与软件 生产就绪 是正交的。你可以在软件准备好之前发布东西(前提是你在自述文件中添加了简短的免责声明)。所以,提前加入适当的发布流程是值得的,这样,当真正发布软件的时候,它只需要移除免责声明和撰写发布帖子,因为所有技术工作早就完成了。

其次,一般来说软件工程观察到了反三角不等式:要从 A 到 C,通过先从 A 到 B,然后从 B 到 C 比直接从 A 到 C 更快。如果你提一个拉请求,有帮助的是将它分成更小的部分。如果你重构某些东西,先引入一个新的工作拷贝然后分别淘汰旧代码,比原地更改这个东西要快。

版本发布没有什么不同:更快、更频繁的版本发布更容易,风险也更小。如果你拥有一套完善的检查措施在你的 NRSR 中,那么每周一次的频率工作得非常好。

从几乎什么都不工作的状态开始要容易得多,但有一个稳固的发布(带一个空的功能集),并从那里开始逐步提升,比起不加考虑地瞎搞,不太考虑最终发布,然后匆忙决定哪些是准备好了且可发布的,哪些应该被削减要好得多。

总结

我想今天就这些了吗?这是很多小点!这里有一个便于参考的项目列表:

  • README 作为一个落地页。
  • 开发文档。
  • 用户文档。
  • 结构化的开发文档(架构和流程)。
  • 未结构化的、优化摄入的开发文档(代码风格,主题指南)。
  • 用户网站,注意内容吸引力。
  • 优化摄入的内部网站。
  • 元文档过程 —— 每个人的任务是增加代码风格和流程文档。
  • 清晰的代码审查协议(球目前在谁的场上?)。
  • 自动检查 git 仓库中没有大型 blob。
  • 不是火箭科学规则。
  • 让我们重复一遍:在所有时候,主分支指向一个已知通过一系列明确定义的检查的提交哈希。
  • 没有半成品测试:如果代码不够好以至于无法添加到 NRSR,它就被删除。
  • 没有不稳定的测试(主要通过 NRSR 的构建)。
  • 单命令构建。
  • 可复现的构建。
  • 固定数量的构建系统入口点。没有单独的 lint 步骤,lint 是一种测试。
  • CI 委托给构建系统。
  • 主要语言中的临时自动化空间。
  • 全面的测试基础设施,项目测试的宏观统一理论。
  • 快速/慢速测试分裂(快速=每个测试套件几秒钟,慢速=每个测试套件低位数分钟)。
  • 快照测试。
  • 基准测试也是测试。
  • 宏观指标追踪(构建时间,测试时间)。
  • Fuzz 测试也是测试。
  • 紧急情况下连续 fuzz 测试结果的水平触发显示。
  • 逆三角不等式。
  • 每周发布。