原子性提交是持续集成与持续交付的必备技能

| 2021-01-28

坚持少做,持续分解,坚持反馈,持续改善”是《持续交付 2.0 》一书中最为重要的四大工作原则。

它们在代码提交与 Code Review 中的应用就是:提交的原子性。

原子性,即 All or Nothing。当你修改完代码,想将其提交,进行Code Review 或合入到团队共有分支时,变更的内容应该包含且仅包含一个不可分割的任务( task )、特性( feature )、修复( fix )或重构( Refector ),同时还要尽可能的小。

术语:CL ,是 Change List 的缩写,代表一次代码变更的集合。

为什么 CL 一定要小

  • 代码评审得更快。对于评审者来说,与预留出 30 分钟的时间来评审一个大段 CL 相比,找到 5 分钟的时间来评审一小段 CL 要容易得多。

  • 评审得更彻底。如果是一个很大的 CL ,评论者和作者往往会面对大量的详细评论感到沮丧,有时甚至会出现非常重要的观点被遗漏的情况。

  • 不太可能引入bug。因为小的 CL 内容少,所以作者和评审者都更容易有效地分析 CL 的影响,并查看是否引入了bug。

  • 如果这个 CL 被拒绝,浪费的工作就更少。如果作者写了一个巨大的 CL ,而评审员却认为:CL 的总体修改方向就是错的,那么,作者可能就浪费了很多工作。

  • CL 更容易合入。一个大的 CL ,需要写很长时间,如果在此期间有很多人提交了代码,那么作者在合并时会有很多冲突,而且必须经常做合并操作。

  • 更容易得出好的代码设计。改进一个小的 CL 的设计和代码健康状况,要比改进一个大 CL 的所有细节容易得多。

  • 减少阻塞,保持工作的连续性。如果这个 CL 是一个完整自包含的原子任务,当你提交评审后,等待评审意见时,你仍旧可以继续下一个原子任务。

  • 让回滚更简单。如果一个 CL 较大,可能改动了较多的文件。当出现问题需要回滚时,我们很可能遇到的一个情况,即:这个大 CL 涉及多个文件,而这些文件在最初的 CL 和要回滚的 CL 之间,还被很多 CL 修改过。这会使回滚操作变得更加复杂(中间的 CL 可能也需要回滚)。

请注意,仅仅因为这次变更太大,审阅者就完全可以直接拒绝您。通常他们会感谢你的贡献,但要求你以某种方式把它变成一系列较小的变化。

然而,假如你已经写完一个大的CL,再去将它拆分成小的提交,可能就是一个很大的工作,或者需要花费大量的时间来争论为什么审阅者应该接受你的大变更。一开始写小的CLs比较容易。

CL 要小到什么程度

​一定要尽最大的努力,让一个 CL 的变更少于 800 行,大部分都应该在400行以下,最好能在 100 行以下

据调查报告显示,每小时最多可以评审 500 行代码,而一次 CR 最好少于 400 行,更容易最大化收益。

如何正确地缩减 CL 的大小

一般来说,CL的正确大小是一个独立的变化。这意味着:

  • 每个 CL 只做了一个非常小的改变,只解决了一件事。

  • 它通常只是一个特性的一部分,而不是一次完成整个特性。CL 太小和太大都不好。你要和你的团队一起,找出一致可接受的尺寸。

  • CL应包括相关的测试代码。如果这个 CL 的确无法加入自动化测试,则需要在提交信息中写上 TestPlan,以便评审者更容易理解。

  • 评审者需要了解关于本次 CR 的所有内容都在这个 CL 、CL 的描述、现有的代码库,或他们已经评审过的CL中。

  • 在本次 CL 合入到代码库以后,不应该破坏系统的正常构建和执行,以便其他开发人员可以正常工作。如果 CL 被部署到生产环境,那么它也不应该给用户带来麻烦。

  • 不能为了保证 CL 的小而让代码难以被评审者难以理解。如果你添加了一个新的 API ,那么就应该在同一个 CL 中包含这个 API 的用法,以便评审者能够更好地理解 API 的使用方式。这还可以防止合入未使用的 API 。

关于什么是“太大”,并没有一个业界公认的硬性规定。100 行 通常是一个 CL 的合理大小,1000 行通常就太大了,但这仍旧取决于评审员的判断。一个变更集所涉及的文件数也会影响它的“大小”。一个文件中 200 行的变更可能没问题,但这200行 散布于 50个文件中,通常就说明太大了。

请记住,尽管您从开始编写代码的那一刻起就与代码密切相关,但评审者通常没有和你一样的上下文知识。对于你来说,一个大小可以接受的 CL ,对你的评审者来说可能就是太大了。评审者很少会抱怨收到的 CLs 太小,如果你真的遵从了前面的建议。

在什么情况下,大的 CLs 是被允许的?

在下面几种情况下,大的 CL 也是可以接受的:

  1. 如果你将某个文件完全删除了,那么即使它有 1000 行代码,你也可以把它看作是 1 行。因为,评审者并不会花很长时间去评审这个被删除的文件的内容。
  2. 有时候,由一个你完全信任的自动重构工具自动生成了一个大的CL,而评审者的只需要验证说它们真的就是该任务想要的变更。这种自动化生产的 CLs 可以更大一些。当然,一定要记住,上面提到的那些要求(比如合并和测试)仍然适用。

如何让 CLs 以正确的方式变小

拆分文件

另一种拆分 CL 的方法是将需要不同评审者的文件分组,但这些文件也是相对独立的变更。

例如:一个 CL 是为了修改协议缓冲区,另一个 CL 是对使用该协议 的代码进行修改。但是,你必须在后一个 CL (使用新的协议)之前提交 第一个 CL(修改协议缓冲区),但它们可以同时被审阅。如果这样做,应该将编写的后一个 CL 通知两组评审者,以便他们有您所做更改的上下文。

另一个例子:一个 CL 用于代码更改,另一个用于对使用该代码的配置或实验进行变更;如果有需要的话,这也更容易回滚,因为配置/实验文件有时推送到生产环境的速度比代码变更要快。

将重构与新增和修复分离

通常最好是从特性修改或bug修复与重构放在不同的 CL 中。例如,移动和重命名一个类应该与修复该类中的 bug 放在两个不同的CL中。当每个 CL 是独立的时,评审者更容易理解它们引入的更改。

不过,在修改功能或修复缺陷的 CL 中可以包含一些小的清理,比如修复局部变量名。假如重构的动作比较大,将它包含在增加功能或修复缺陷的 CL 中,会使评审变得更加困难。

实在无法让 CLs 更小,怎么办?

有时你会遇到这样的情况,你的 CL 似乎必须很大。但是,这很少是事实。练习编写小型 CLs 的作者几乎总能找到将功能分解为一系列小更改的方法。

如果你认为,这次变更一定是一个大的 CL 时,请先考虑是否在它前面加上一个只有重构操作的 CL ,以便为后续能更清晰干净的修改代码铺平道路。与团队成员讨论一下,看看其他人是在使用小型 CLs 实现该功能有好的想法。

如果上面的这些选项都不行(这应该是非常罕见的),那么请事先征得评审者的同意,这样,他们就会被警告即将发生的事情。在这种情况下,我们需要花很长时间来检查这个过程,保持警惕,不要引入bug,并且在编写测试时应格外努力。

其它注意事项

不要让构建失败

如果有几个相互依赖的CL,那么我们就需要找到一种方法,来确保在提交每个 CL 之后,整个系统都能正常工作。否则,很可能你会在 CLs 提交之间,阻断其他开发人员的构建。如果您稍后的 CL 提交出现意外错误,甚至会让整个团队的工作阻滞更长时间。

如果一次 CL 提交了一个失败的测试用例,那么这个 CL 也不算是一个完整的工作单元。

CL 应该包含与其相应的测试代码

CLs应包括相关的测试代码。请记住,这里的“小”指的是概念上的小,即 CL 应该是聚焦的,而不是修改一个变更名,或者添加一个空函数体。

添加或修改代码逻辑的 CL 应该伴随着针对新行为的新的测试用例,或更新原有的测试用例。纯重构 CLs(不打算改变行为)也应该包含在测试中;理想情况下,这些测试已经存在。如果没有测试,,你应该添加上。

对于独立的测试修改,可以放在第一个单独的 CL 上,类似于前面的重构指南。这包括:

  • 使用新的测试用例验证预先存在的、已提交的代码。
    • 确保重要的逻辑被测试用例覆盖。
    • 增加对受影响代码的在后续重构中的信心。例如,如果要重构那些没有测试用例覆盖的代码,则在提交重构 CLs 之前,先提交测试 CLs,以验证重构前后测试行为是否保持不变。
  • 对测试代码进行重构(例如引入 helper 函数)。
  • 引入更大的测试用例(例如集成测试)。
参考链接:

谷歌工程实践