问题的提出
任何复杂的软件都是团队工作的产物,所以我们会利用版本控制工具和不同的分支策略来协助团队的日常开发和交流,mainline开发模式和pull request开发模式(以下简称PR)则是最常用到的两种模式。在开发时选择哪种模式也成了一个经常被讨论的话题。
在疫情时代,远距离办公可能会阻碍团队的交流,PR开发模式也变得越来越流行。一方面PR开发模式可以为代码开发带来更好的隔离性,但另一方面,PR开发模式其实是一种更难掌握或者说要求更高的开发模式。比如:审查和合并 PR 的速度至少取决于三个因素:上下文、大小和原子性。此外,PR开发模式对重构不是很友好。因为重构需要高频率的集成来尽早发现和解决引入的冲突,但在PR开发模式之下是比较难做到的。
相比之下,mainlina开发模式是我更为倾向的一种实践。首先,它不需要考虑太多额外的因素。开发人员只需要有了“健康”的commit之后,就能与mainline集成,将自己的代码变更在团队中可视化。此外,它对重构有很好的支持,因为mainline开发模式本身就支持持续集成。
《Software Configuration Management Patterns》(以下简称SCM patterns)一书从软件配置管理的角度出发,关注那些会影响代码编写、功能实现以及代码更改等日常工作的各个方面,并将其总结成一系列模式。
下面,本文想要从SCM patterns的视角来对比一下mainline开发模式和PR开发模式。
概念的厘清
为了更好地理解这两种开发模式,以及将其进行对比,有必要先对相关的基本概念以及两种模式的概念进行简要的说明。
Codeline和Codeline Policy组合差异决定开发模式的不同
不同的codeline和codeline policy的组合可以形成不同的开发模式。从形式上看,mainline开发模式和PR开发模式的区别,其实是codeline和codeline policy的不同。因此,我们有必要先了解一下这二者的基本概念。
Codeline
codeline其实就是我们常说的branch(Brad Appleton在这里有做说明),在SCM patterns中,codeline的定义如下:
A codeline is a progression of the set of source files and other artifacts that make up some software component as it changes over time. Every time you change a file or other artifact in the version control system, you create a revision of that artifact.
Codeline Policy
codeline policy实际上是对于codeline的使用手册,为每一条codeline持续运行提供了保障机制,也能够让开发人员更加明确的知道:应该将代码签入哪个codeline、何时签入以及在签入前要运行哪些测试。每个codeline都会相应地有一个codeline policy。
举个例子:
Development codeline:可以签入临时代码,但相关组件需要是可以构建的。
Mainline:所有组件必须编译和链接,并通过回归测试;已完成并且经过测试的新功能可以签入。
Release codeline:软件必须在签入前构建并通过回归测试;签入的代码仅限于错误修复;不得签入新特性或功能;签入后,分支被冻结,直到整个QA 周期完成。
总的来说,不同的codeline在不同的项目中,有不同的目的和不同的codeline policy,同时也代表着不同的稳定程度,比如:active development codeline是以快速开发为主,稳定程度足够开发就好;而release codeline则是一个完全测试,必须保持足够稳定的codeline。相比之下,release codeline对稳定程度的要求则高很多。
对待mainline和active development line的方式决定两种开发模式的不同
了解完上面的基本概念之后,下面我们来正式认识一下这两种不同的开发模式。mainline开发模式和PR开发模式两者明显的区别是对待mainline和active development line的方式不同。
Mainline
mainline是一个特殊的codeline,通常它被认为是代表了团队代码的当前状态,用作子分支及其合并的基础。Martin Flower是这样描述mainline的:
A single, shared, branch that acts as the current state of the product
需要注意的是mainline是一个codeline,而mainline开发模式是一种development model。
Active development line
active development line是开发人员开发代码所使用的codeline/branch。但这个codeline的特点是:足够稳定,能保证开发即可。
Mainline开发模式
mainline开发模式是一种开发人员在mainline上直接进行开发工作的模式。此时,mainline = active development line。在SCM patterns中,作者是这样描述mainline development model的:
When you are developing a single product release, develop off of a mainline. A mainline is a “home codeline” that you do all your development on except in special circumstances.
Pull request 开发模式
PR开发模式则是开发人员在分支上进行开发,然后通过pull request的方式,将分支合并回mainline的一种开发模式。此时,mainline != active development line。
两种模式的对比
通过上面对两种开发模式的介绍,我们知道两者一个明显的区别是mainline和active development line是否为同一codeline。那么接下来,我想通过集成设计、codeline稳定程度的变化和出发点这三个方面来对两种开发模式进行对比。
mainline开发模式的集成设计比PR开发模式更为简单
首先,我们来看codeline和CI设计的问题。
在mainline开发模式中:
mainline = active development line
如果需要一个非常稳定的代码线(stable codeline)以做发布相关或已发布的bug修复等工作的时候,通常会切一个release codeline,此时:
release codeline = stable codeline
在PR开发模式中:
feature branch = active development line
注:feature branch在这里指PR所对应的那个分支
通常,采用PR开发模式其实就是因为mainline需要非常稳定,所以此时:
mainline = stable codeline
因此,这两种模式对mainline的稳定程度要求是不同的,mainline开发模式对mainline的稳定程度要求是低于PR开发模式的。此外,codeline和CI的联系是非常紧密的,因为CI的触发来自于代码的改变,而代码的改变来自于特定codeline的commit。所以,在考虑设计我们的CI的时候,同样也需要考虑如何设计我们的codeline。在SCM patterns的视角下,就是对private workspace的要求不同。
注:图片来源于《Software Configuration Management Patterns》官方网站
在SCM patterns这张图中,我们可以看到:private workspace模式的"Complete with"需要integration build和private system build等。而integration build和private system build都需要一系列测试的支持(smoke test、unit test、regression test)。
在mainline开发模式中,开发人员通常是在本地完成开发并通过了codeline policy的提交要求之后,直接将代码合并到mainline。这个过程中,开发人员会进行频繁地集成,与mainline的集成通常是在一天之内。由于我们的active development line(mainline)在远端只有一条,只有当我们进行mainline上的提交的时候,才会触发CI的运转。这个时候,我们CI数量等于mainline的数量,其实也就是1条CI。
但在PR开发模式中,开发人员通常是在PR被approve之后,才能对mainline进行集成,因此集成时间通常是好几天甚至一两周。这个时候,由于mainline = stable line,但因为feature branch不会对mainline频繁集成,为了避免feature branch在集成时对stable line引入严重的错误,因此也会要求feature branch能够进行更多的测试。但更多的测试也往往意味着更多的开销,为了让本地开发高效进行,通常会把build和test放到CI上去。因而在PR开发模式下,通常是一个PR(feature branch/active development line)对应一个CI,这个时候,我们CI数量等于mainline + feature branch的数量。
由此可见,mainline开发模式和PR开发模式对CI的设计是不同,其根本在于codeline的稳定程度影响着对private workspace的要求。
注:把mainline上的代码持续合并代码到PR的active development codeline不叫集成,因为集成是pull和push双方向,Martin Flower的《Patterns for Managing Source Code Branches》文章里也有对此做说明。
mainline开发模式中mainline的稳定程度在持续集成下比PR开发模式更容易发生变化
虽然在mainline开发模式下,mainline = active development line,我们对mainline的稳定程度要求可能不如stable line/release line这么高。但由于频繁持续的集成,其实mainline/active development line的稳定程度是会发生改变并且趋向于稳定的,而在PR模式下,active development line和mainline的稳定程度则不会一起变化。
在进行对比之前,我们先来看Laura Wingerd在她的书《Practical Perforce》中提出的一种“tofu”模型。“tofu”模型是对codeline的稳定性和质量的一种非正式评估。“tofu”模型会根据以下四个方面来对codeline进行度量:
- How close software is to being released
- How rigorously changes must be reviewed and tested
- How much impact a change has on schedules
- How much a codeline is changing
注:图片来源于《Practical Perforce》
从图上我们能看见,release codeline在“tofu”模型上处于最高(firm),通常来说,它不会有太大的变化,也会有严格的审查和测试要求,即使是轻微的变化也可能会影响发布计划。mainline是中等,代码的更改会需要通过测试,但离发布时间更远,对发布计划的影响处于中等。development codeline处于最低(soft),往往会变化迅速,离软件发布最远,甚至可能还没有针对其最新开发的测试。
现在回过头来看我们这两种开发模式。
在PR开发模式中,mainline和active development line本身就是两条codeline,对应的policy也不相同。这里或许会说,我将mainline的policy应用到active development line上,试图提升active development line在“tofu”模型中的位置,从而间接提升mainline的位置。但其实开发人员始终是在active development line上开发,而非mainline。在active development line没有被集成到mainline之前,它们始终是两条codeline。另外,PR开发模式和持续集成也是割裂的,因为PR决定了集成频率的上限,只有在PR被approve之后才能被集成到mainline中。这种割裂则让PR开发模式下的mainline难以享受持续集成带来的好处,比如:更早的发现和解决问题以减少风险。因此,在PR开发模式下,mainline和development line始终会是处于一高(mainline)一低(development line)的状态,无论development line多么靠近mainline。
但在mainline开发模式中,mainline和active development line是同一条codeline。那么在“tofu”模型中,由于是同一条codeline,这也就意味着将development line/mainline的上限从mainline变成了release line。其次,由于mainline开发模式本身对持续集成就有着很好的支持,其稳定程度也在不断地集成中趋向于稳定。因为代码的更改会非常频繁地集成到mainline中,每一次的合并实际上都是对mainline质量的一次考验。此外,如果大家能坚持执行“一旦有失败的commit,所有开发人员的第一优先级是修复错误并且不能向mainline提交commit”的理念,那么mainline的质量会趋向于维持在一个较高的水平。
mainline开发模式的出发点比PR开发模式更有利于团队的长期发展
mainline开发模式和PR开发模式都需要维护代码质量,但这两种模式却又有着不同的出发点。mainline开发模式的出发点是对团队内开发人员的信任,外加自动化测试套件来维护代码质量,而PR开发模式的出发点是对陌生开发人员的不信任,从而需要依赖于人工审核来维护代码质量。
基于不同的出发点,我们也常常能看见二者被使用到不同的项目之中。开源项目和商业项目则是非常典型的例子,不同的项目背景往往也有着不同的组织结构。
开源项目通常是由一些受信任核心成员和不受信任的陌生的开发人员组成。任何陌生人都能接触到开源项目的源码,并且通过创建分支,提交PR的形式,对项目进行贡献。而这些工作,在提交PR之前,项目中的核心成员,对这个人和这个PR完成所需要的时间都是不确定或者说无感知的。PR开发模式很好地解耦了mainline与不受信任开发者之间的依赖关系,既不影响现有软件的状态,同时又为想要对软件作出贡献的人提供了较低的门槛,并且保证了核心成员对软件的质量的充分话语权。
商业项目则不同,其通常是由技术领导人和一些开发人员组成。技术领导人对团队中的开发人员会有一定的了解,并且对一个功能大概会在什么时间内完成也会有相应的计划,整个团队通常具有一定的信任度基础。
因此,PR开发模式常见于开源项目中,而mainline开发模式则常见于商业项目中。
在受信任的团队氛围中工作通常既有利于团队发展,对个人也是一种鼓励。因此,mainline开发模式对开发人员更为友好。由于团队信任度并非一成不变,因此在某些时候,使用PR开发模式也不失为一种好方法。
以我个人的经历举例:在我刚开始参加工作,成为一名开发人员的时候,当时项目上正好使用PR开发模式,我会经常去找经验丰富的前辈帮忙看自己的PR。不同于code review,PR往往给予代码审核者一个更加全面的视角,因为不限于code review的时间,审核人员也能根据自己的思路对整个PR的实现代码进行详细的理解,因此,PR的审核相较于code review会更加仔细,对于代码质量有更加严格的控制;而对于被审核者,根据相应的指导和建议进行代码修改,也能较快地提升代码的能力。但在加入项目一年多以后,大家对彼此更加了解和信任之后,则采取了mainline开发模式和code review的相对较宽松的方式来对代码质量进行管理。
结语
尽管在当下随着远距离办公更为普遍,PR开发模式越来越流行,我们仍然需要小心谨慎地使用它。在使用PR开发模式的时候,我们需要合理决定PR的大小,避免PR过大导致将codeline演变成feature branch。同时,团队需要提出合理的机制以保证PR审核的及时性,否则这种开发模式将极大地阻碍团队的开发效率。
相比之下,我个人更倾向于mainline开发模式,它在团队交流上具有天然的优势。开发人员之间其实是通过集成来进行交流的。怎么快速知道别人的代码更改会不会影响我,怎么快速知道我的代码对mainline产生的影响,怎么将自己的代码放到大家可见的地方以让别人知道我的更改。这一系列的问题的根本解决方式就是:与mainline快速集成。其次,关于集成的设计成本也较PR开发模式的一个PR(active development line)对应一条CI更低。最后,在谈到团队信任氛围上,mainline开发模式的出发点是以营造一个相互信任的开发环境,并同时赋予每一个开发人员自由向mainline提交代码的权利,这是一种更加健康和可持续发展的方式。
参考
- Software Configuration Management Patterns
- Martin Flower: Patterns for Managing Source Code Branches
- Wait For It: Determinants of Pull Request Evaluation Latency on GitHub
- Laura Wingerd: High-Level Best Practices in Software Configuration Management
- Concepts in configuration management systems
- Practical Perforce
- Understanding the connection between branching models and delivery pipeline
- Getting Faster Pull Requests in an Agile Environment
——————
文/Thoughtworks 郑茗蔓
原文链接:对比mainline和pull request的开发模式-Thoughtworks洞见