小 CL
目录
为什么应该写小 CL?{#why}
小 CL 有如下优点:
- 审核更快。 相对让审核者单独拿出30分钟审核大 CL,不如让他花费几个5分钟审核代码。对审核者而言,后者更容易。
- 审核更彻底。 发生较大修改时,往往会反复审核、修改,审核者与开发者经常会因为过多的反复而在情绪上受到影响,以致于把精力放在修改上了,却忽略了 CL 中更重要的部分。
-
引入新缺陷的可能性更小。 如果修改的内容比较少,自然审核人的效率会更高,开发者与审核者都更容易判断是否引入了新的缺陷。
-
如果被拒绝,浪费的时间更少。 如果开发者花费了很大的精力开发了一个大 CL,直到审核的时候才知道整个开发的方向错了,那么之前的所有时间就全浪费了。
-
更容易合并代码。 大 CL 在合并代码时会花费很长的时间,在合并时需要花费大量时间,而且在写 CL 期间可能不得不频繁地合并。
-
更易于设计。 完善小 CL 的设计和修改要容易得多,多次微小的代码质量提高比一次大的设计改变更容易。
-
减少阻塞审核的可能性。 小 CL 通常是功能独立的部分,你可能正在修改很多代码(多个小 CL),在发送一个 CL 审核时,同时可以继续修改其他的代码,并不会因为当前 CL 的审核没有完成而阻塞。
- 更容易回退。 一个大 CL 开发的时间比较长,这意味从开发到代码提交这段期间,代码文件的变更会比较多。当回退代码时,情况会变得很复杂,因为所有中间的 CL 很有可能也需要回退。
请注意:审核者有权因为你的 CL 太大而拒绝它。 通常,他们会感谢你为代码做出的贡献,但是会要求你把它拆分多个小 CL。一旦写好了代码之后,要把它拆分成小 CL 通常需要花很多时间,当然,你也可能会花费大量时间与审核者争论为什么他应该接受这个大 CL 。与其如此,不如设计之初就保证 CL 尽量小。
如何定义“小”?
一般而言,一个 CL 的大小就应该是 独立功能的修改。这意味着:
-
一个 CL 尽量最小化,它只 做一件事。通常它只是一个功能的一部分,而不是整个功能。总体而言,CL 太小或太大都不好,二者取其轻的话,太小稍微好点。可以与审核者一起讨论,找出大多比较合适。
-
审核者需要理解 CL 中包含的一切(除了以后可能要开发的功能之外),包括 CL 代码、描述、已存在的代码(或之前已经审核过的相关 CL )。
-
在 CL 代码提交之后,无论是针对用户,还是针对开发人员,系统应该仍旧运行良好。
-
如果代码难以理解,通常是因为 CL 还不够小。如果新增一个 API,同时应该同一个 CL 中附上这个 API 的使用方法,便于审核者理解如何使用,也方便以后的开发者理解。同时也可能有效防止提交的 API 无人使用。
没有直观的标准判断 CL “太大”应该符合哪些条件。 100行代码通常是一个合理的大小。1000行代码通常太大了,但也不能一概而论,这取决于审核者的判断。修改文件的个数也影响它的“大小”。在一个文件中修改200行可能没问题,如果这200行代码横跨50个文件,通常而言太大了。
记住,当你开始编写代码时,只有你最了解代码,而审核者通常不了解上下文。在你看起来很是一个合适大小的 CL,审核者可能会很困惑。毫无疑问,在写 CL 时,CL 的大小最好比自认为的还要小。审核者通常不会抱怨你的 CL 太小了。
什么时候可以有大 CL?
当然,也有一些例外情形,允许 CL 比较大:
-
删除一个文件与修改一行没有太大区别, 因为它不会花费审核者太多时间。
-
有时候,一个大 CL 可能是由可靠的自动代码重构工具生成的,审核者的工作主要是检查它是否做了它应该做的工作。虽然符合以上提到的注意事项(例如合并和测试),这类 CL 也可能比较大。
以队列的方式,一个接一个提交 CL
一种拆分 CL 的方式是:在不阻塞自己的代码的前提下,编写完一个 CL 后,提交它便于审核,然后立即 基于 第一个 CL开始写下一个 CL。大多数版本控制系统都允许你以这种方式工作。
以文件来拆分
另外一种拆分大 CL 的方法是:对 CL 中涉及的文件进行分组,这就要求不同独立功能的修改需要相应的审核者。
例如:你提交了一个 CL,这个 CL 修改了协议缓冲区,而且另外一个 CL 用到它。因此我们先提交第一个 CL,再提交第二个 CL,并让两个 CL 同时审核。此时,你应分别向两个 CL 的审核者告知另外一个 CL 的内容,以便他们知道上下文。
以代码和配置文件进行拆分。例如,你提交了2个 CL:其中一个 CL 修改了一段代码,另外一个 CL 调用了这段代码或代码的相关配置;当需要代码回滚时,这也比较容易,因为配置或调用文件有时候推送到产品比代码修改相对容易。
水平拆分
考虑创建共享代码或桩代码,用于帮助隔离技术栈各层之间的变化。这不仅有助于加快开发速度,而且还鼓励层之间的抽象。
例如:你创建了一个计算器程序,它包含客户端、API、服务和数据模型层。共享原型签名可以抽象服务层和数据模型层。同样,API桩可以将客户代码的实现从服务代码分离开,确保它们相互独立。类似的想法还可以应用于更细粒度的函数类级别的抽象。
垂直拆分
与分层的水平拆分方式正交,可以将代码拆分成更小的、全栈的、垂直功能。这些功能中的每一个都可以独立并行实现。这使得当某些轨道在等待审核或反馈时,另一些轨道可以继续前进。
回到水平拆分中的计算器示例。现在你想支持新的运算符,如乘法和除法。在实现时,你可以把乘法和除法实现为单独的垂直或子功能,即使他们有些功能重合,如 共享按钮样式或共享验证逻辑。
水平和垂直拆分
更进一步,你还可以把两种方式相结合,以如下表来实现。在下表中,每个单元格是一个独立的 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 |
单独重构
在修改功能或修复缺陷的 CL 中,不建议把重构也加进来,而是建议把它放到单独的 CL 中。例如,修改类名或把某个类移到其他包内是一个 CL,修复这个类中的某个缺陷是一个 CL,不要把它们合并到一个 CL 中。把它们拆分出来更有利于审核者理解代码的变化。
有些代码清理工作,如修改某个类中的一个变量的名称,可以把它包含在一个功能修改或缺陷修复的 CL 中。那标准是什么呢?这取决开发者与审核者的判断,这种重构是否大到让审核工作变得很困难。
把测试代码包含到对应功能的 CL 中
避免单独提交测试代码。测试代码用以验证代码功能,应该把它与代码提交到相同的 CL 中,虽然它会增大 CL 的代码行数。
然而,独立的测试修改可以放到单独的 CL 中,这与单独重构中的观点比较类似。它包含如下内容:
- 为过去提交的已存在代码创建新的测试代码。
- 重构测试代码(例如,引入 helper 函数)。
- 引入测试框架代码(如,集成测试)。
不要破坏编译
如果同时在审核的有多个 CL,并且这些 CL 之间存在依赖关系,你需要找到一种方式,确保依次提交 CL 时,保证整个系统仍旧运行良好。否则,可能在提交某个 CL 之后,让系统编译错误。此时,你的同事在更新代码后,不得不花时间查看你的 CL 历史并回退代码以确保本地编译没有问题(如果你之后的 CL 提交出了问题,可能会花费更多时间)。
无法将其变小
在某些情形下,好像你没法然 CL 变得更小,这种情况很少发生。如果开发者经常写小 CL,那么他往往都能找到一种把 CL 拆得更小的方法。
如果在写代码之前就估计这个 CL 比较大,此时应该考虑是否先提交一个代码重构的 CL,让已有的代码实现更清晰。或者,与团队其他成员讨论下,看是否有人能帮你指出,怎样在提交小 CL 的前提下实现当前功能。
如果以上所有方法都试过,还是不可行(当然,这种情况比较罕见),那就先与所有的审核者沟通一下,告知他们你将会提交一个大 CL,让他们先有心理准备。出现这种情况时,审核过程往往会比较长,同事需要写大量的测试用例。需要警惕,不要引入新的 bug。
下一章: 如何处理审核者的评论