# 小 CL

## 为什么应该写小 CL？ <a href="#why" id="why"></a>

小 CL 有如下优点：

* **审核更快。** 相对让审核者单独拿出30分钟审核大 CL，不如让他花费几个5分钟审核代码。对审核者而言，后者更容易。
* **审核更彻底。** 发生较大修改时，往往会反复审核、修改，审核者与开发者经常会因为过多的反复而在情绪上受到影响，以致于把精力放在修改上了，却忽略了 CL 中更重要的部分。
* **引入新缺陷的可能性更小。** 如果修改的内容比较少，自然审核人的效率会更高，开发者与审核者都更容易判断是否引入了新的缺陷。
* **如果被拒绝，浪费的时间更少。** 如果开发者花费了很大的精力开发了一个大 CL，直到审核的时候才知道整个开发的方向错了，那么之前的所有时间就全浪费了。
* **更容易合并代码。** 大 CL 在合并代码时会花费很长的时间，在合并时需要花费大量时间，而且在写 CL 期间可能不得不频繁地合并。
* **更易于设计。** 完善小 CL 的设计和修改要容易得多，多次微小的代码质量提高比一次大的设计改变更容易。
* **减少阻塞审核的可能性。** 小 CL 通常是功能独立的部分，你可能正在修改很多代码（多个小 CL），在发送一个 CL 审核时，同时可以继续修改其他的代码，并不会因为当前 CL 的审核没有完成而阻塞。
* **更容易回退。** 一个大 CL 开发的时间比较长，这意味从开发到代码提交这段期间，代码文件的变更会比较多。当回退代码时，情况会变得很复杂，因为所有中间的 CL 很有可能也需要回退。

请注意：**审核者有权因为你的 CL 太大而拒绝它。** 通常，他们会感谢你为代码做出的贡献，但是会要求你把它拆分多个小 CL。一旦写好了代码之后，要把它拆分成小 CL 通常需要花很多时间，当然，你也可能会花费大量时间与审核者争论为什么他应该接受这个大 CL 。与其如此，不如设计之初就保证 CL 尽量小。

## 如何定义“小”？ <a href="#what-is-small" id="what-is-small"></a>

一般而言，一个 CL 的大小就应该是 **独立功能的修改**。这意味着：

* 一个 CL 尽量最小化，它只 **做一件事**。通常它只是一个功能的一部分，而不是整个功能。总体而言，CL 太小或太大都不好，二者取其轻的话，太小稍微好点。可以与审核者一起讨论，找出大多比较合适。
* CL 应该包含[相关的测试用例](#test_code)。
* 审核者需要理解 CL 中包含的一切（除了以后可能要开发的功能之外），包括 CL 代码、描述、已存在的代码（或之前已经审核过的相关 CL ）。
* 在 CL 代码提交之后，无论是针对用户，还是针对开发人员，系统应该仍旧运行良好。
* 如果代码难以理解，通常是因为 CL 还不够小。如果新增一个 API，同时应该同一个 CL 中附上这个 API 的使用方法，便于审核者理解如何使用，也方便以后的开发者理解。同时也可能有效防止提交的 API 无人使用。

没有直观的标准判断 CL “太大”应该符合哪些条件。 100行代码通常是一个合理的大小。1000行代码通常太大了，但也不能一概而论，这取决于审核者的判断。修改文件的个数也影响它的“大小”。在一个文件中修改200行可能没问题，如果这200行代码横跨50个文件，通常而言太大了。

记住，当你开始编写代码时，只有你最了解代码，而审核者通常不了解上下文。在你看起来很是一个合适大小的 CL，审核者可能会很困惑。毫无疑问，在写 CL 时，CL 的大小最好比自认为的还要小。审核者通常不会抱怨你的 CL 太小了。

## 什么时候可以有大 CL？ <a href="#large_okay" id="large_okay"></a>

当然，也有一些例外情形，允许 CL 比较大：

* 删除一个文件与修改一行没有太大区别， 因为它不会花费审核者太多时间。
* 有时候，一个大 CL 可能是由可靠的自动代码重构工具生成的，审核者的工作主要是检查它是否做了它应该做的工作。虽然符合以上提到的注意事项（例如合并和测试），这类 CL 也可能比较大。

## 高效地编写小 CL <a href="#efficiently" id="efficiently"></a>

如果编写了一个小 CL，然后等待代码审核者审阅你的代码，待审核之后再编写下一个 CL，那无疑会浪费太多时间。所以，你应该找一些在审核是不会阻塞你的工作方式。例如，同时处理多个项目，寻找可以立即审视代码的审核者，面对面审核，结对编程，或以一种允许你立即继续工作的方式拆分 CL。

## 拆分 CL <a href="#splitting" id="splitting"></a>

当开始处理多个 CL 且这些 CL 有潜在依赖关系时，在深入编程之前，应在更高层次上考虑如何拆分合组织这些 CL。

除了让开发者更轻松地管理和组织 CL 之外，它还让代码审核者更轻松，从而让代码审核更高效。

以下是拆份 CL 的一些策略。

### 以队列的方式，一个接一个提交 CL <a href="#stacking" id="stacking"></a>

一种拆分 CL 的方式是：在不阻塞自己的代码的前提下，编写完一个 CL 后，提交它便于审核，然后立即 _基于_ 第一个 CL开始写下一个 CL。大多数版本控制系统都允许你以这种方式工作。

### 以文件来拆分 <a href="#splitting-files" id="splitting-files"></a>

另外一种拆分大 CL 的方法是：对 CL 中涉及的文件进行分组，这就要求不同独立功能的修改需要相应的审核者。

例如：你提交了一个 CL，这个 CL 修改了协议缓冲区，而且另外一个 CL 用到它。因此我们先提交第一个 CL，再提交第二个 CL，并让两个 CL 同时审核。此时，你应分别向两个 CL 的审核者告知另外一个 CL 的内容，以便他们知道上下文。

以代码和配置文件进行拆分。例如，你提交了2个 CL：其中一个 CL 修改了一段代码，另外一个 CL 调用了这段代码或代码的相关配置；当需要代码回滚时，这也比较容易，因为配置或调用文件有时候推送到产品比代码修改相对容易。

### 水平拆分 <a href="#splitting-horizontally" id="splitting-horizontally"></a>

考虑创建共享代码或桩代码，用于帮助隔离技术栈各层之间的变化。这不仅有助于加快开发速度，而且还鼓励层之间的抽象。

例如：你创建了一个计算器程序，它包含客户端、API、服务和数据模型层。共享原型签名可以抽象服务层和数据模型层。同样，API桩可以将客户代码的实现从服务代码分离开，确保它们相互独立。类似的想法还可以应用于更细粒度的函数类级别的抽象。

### 垂直拆分 <a href="#splitting-vertically" id="splitting-vertically"></a>

与分层的水平拆分方式正交，可以将代码拆分成更小的、全栈的、垂直功能。这些功能中的每一个都可以独立并行实现。这使得当某些轨道在等待审核或反馈时，另一些轨道可以继续前进。

回到[水平拆分](#splitting-horizontally)中的计算器示例。现在你想支持新的运算符，如乘法和除法。在实现时，你可以把乘法和除法实现为单独的垂直或子功能，即使他们有些功能重合，如 共享按钮样式或共享验证逻辑。

### 水平和垂直拆分 <a href="#splitting-grid" id="splitting-grid"></a>

更进一步，你还可以把两种方式相结合，以如下表来实现。在下表中，每个单元格是一个独立的 CL。从模型（底部）开始开发，最后是顶部的客户端。

| 层       | 功能: 乘法                    | 功能: 除法                                         |
| ------- | ------------------------- | ---------------------------------------------- |
| Client  | Add button                | Add button                                     |
| API     | Add endpoint              | Add endpoint                                   |
| Service | Implement transformations | Share transformation logic with multiplication |
| Model   | Add proto definition      | Add proto definition                           |

## 单独重构 <a href="#refactoring" id="refactoring"></a>

在修改功能或修复缺陷的 CL 中，不建议把重构也加进来，而是建议把它放到单独的 CL 中。例如，修改类名或把某个类移到其他包内是一个 CL，修复这个类中的某个缺陷是一个 CL，不要把它们合并到一个 CL 中。把它们拆分出来更有利于审核者理解代码的变化。

有些代码清理工作，如修改某个类中的一个变量的名称，可以把它包含在一个功能修改或缺陷修复的 CL 中。那标准是什么呢？这取决开发者与审核者的判断，这种重构是否大到让审核工作变得很困难。

## 把测试代码包含到对应功能的 CL 中 <a href="#test_code" id="test_code"></a>

CL 应该包含相关的测试代码。谨记，这里提到的[小](#what-is-small)的含以是 CL 应该聚焦于很小的功能，而不应简单粗暴地通过代码行数来衡量。

在 Google，所有的代码修改都应包含测试代码。

当新增或修改逻辑时，应同时更新测试。纯重构 CL（不涉及改变行为），应保证代码修改不会破坏已有的测试用例。理想情况下，在修改之前应该存在测试代码。如果不存在，则应添加。

然而，_独立的测&#x8BD5;_&#x4FEE;改可以放到单独的 CL 中，这与[单独重构](#refactoring)中的观点比较类似。它包含如下内容：

* 为已存在的代码创建新的测试代码。
  * 确保重要的逻辑有对应的测试代码。
  * 增加对受影响代码进行后续重构的信心。例如，你想重构测试尚未覆盖的代码，则在提交重构 CL 之前提交测试 CL，用于验证重构前后，代码行为是否发生改变。
* 重构测试代码（例如，引入 helper 函数）。
* 引入测试框架代码（如，集成测试）。

## 不要破坏编译 <a href="#break" id="break"></a>

如果同时在审核的有多个 CL，并且这些 CL 之间存在依赖关系，你需要找到一种方式，确保依次提交 CL 时，保证整个系统仍旧运行良好。否则，可能在提交某个 CL 之后，让系统编译错误。此时，你的同事在更新代码后，不得不花时间查看你的 CL 历史并回退代码以确保本地编译没有问题（如果你之后的 CL 提交出了问题，可能会花费更多时间）。

## 无法将其变小 <a href="#cant" id="cant"></a>

在某些情形下，好像你没法然 CL 变得更小，这种情况很少发生。如果开发者经常写小 CL，那么他往往都能找到一种把 CL 拆得更小的方法。

如果在写代码之前就估计这个 CL 比较大，此时应该考虑是否先提交一个代码重构的 CL，让已有的代码实现更清晰。或者，与团队其他成员讨论下，看是否有人能帮你指出，怎样在提交小 CL 的前提下实现当前功能。

如果以上所有方法都试过，还是不可行（当然，这种情况比较罕见），那就先与所有的审核者沟通一下，告知他们你将会提交一个大 CL，让他们先有心理准备。出现这种情况时，审核过程往往会比较长，同事需要写大量的测试用例。需要警惕，不要引入新的 bug。

下一章: [如何处理审核者的评论](handling-comments)
