对于单一贡献者来说,由于 pull 请求与功能分支的存在周期较短,所以处理起来难度不大。但面对大规模团队,这种方式就往往显得不那么高效了。
对于大多数规模化团队,直接使用 master 往往效率更高,这是因为他们必须以安全方式进行主干开发,从而提高协作水平、增强开发质量以及实现集体所有权等等。
基于主干开发的优势
“不,我们绝对不要直接对 master 进行 push,你疯了吗?”
好吧,我来讲个真实的故事。
我最近加入了一家咨询企业。担任顾问职务的乐趣在于,我们可以与各类客户一同工作。事实上,在最近几个月的实际工作当中,我已经与 3 个不同的团队进行过合作。团队成员往往身处同一地点,规模约为 6 到 8 人,主要由工程师以及产品负责人组成,有时还设有 Scrum Master 职位。团队成员几乎围坐在一起,当然偶尔也会有人选择在家里远程办公。
在刚刚加入团队时,我总会提出很多问题,主要是为了了解项目的基本情况以及如何为成员们提供帮助。在这 3 个团队当中,我们总会谈到版本控制,而且对话基本都遵循以下套路:
我:“好吧,我输入代码之后,会发生什么情况?”
他们:“你会创建一条 pull 请求,把对应的链接放进 Slack 里,然后会有人进行审查。”
我:“哦,所以不能直接对 master 进行 push 喽?”
他们:睁大眼睛看着我,就像盯着外星人……然后就是几秒钟的沉默
他们:不以为然地摇摇头“当然不行,我们不会直接 push 到 master。代码需要首先接受审查。”
这时候,我能感受到他们正在想:“这家伙是认真的吗?这人真的是来帮忙的吗?”
当然,我不介意受到质疑,也不介意人们拒绝我的建议,毕竟这也是我日常工作的组成部分。但最令人感到惊讶的是,几乎没有人会把直接 push 到 master 看作一个可行的选项。我在谷歌上搜索过很多次,也问过其他公司的工作人员,看起来功能分支确实已经成为一种常态,并在软件开发行业中成为一大既定标准。
但我原先所在的公司主张直接 push 到 master,所以我感觉非常矛盾,并决定写下这篇博文。我希望解释一下,为什么我坚信使用临时功能分支的团队应该试试直接对接 master,以及这种作法能够带来怎样的收益。
团队如何使用临时功能分支与 pull 请求
首先,我要解释一下我观察到的使用临时功能分支的团队的典型工作流程:
团队积压了一大堆待办工作。一般来说,他们会整理出“积压冲刺”清单,其中包含大量需要在下一冲刺期内解决的工作。(顺带一提,我对处理这类问题也有一些自己的想法,将在后文中与大家分享。)
这时有一位开发人员比较清闲,所以团队开始审视积压清单并选定一项工作。(这个选择的过程各不相同,这往往代表着团队中存在不少问题。我们将在后文中具体讨论。)
这位开发人员从 master 创建一个新分支,而后开始处理这项积压工作。
在工作时,团队会根据自己习惯的频率将代码 push 至该分支,可能是几分钟一次、几小时一次或者几天一次。
当代码 push 完成后,构建工具(Jenkins、circleci 等)会在该分支上运行 build。一般来讲,这个 build 会首先运行基本测试,而后编译并打包相关代码。有时候还可能需要将其部署至开发环境。
在开发工作结束后(整个周期可能耗时数天或者一周左右),这位开发人员会创建一条 pull 请求。
团队中的其他某人审查这条 pull 请求。在某些团队中,任何人都能够查看这条请求;但在其他一些团队中,则必须由技术负责人或者高级开发人员进行审查。
审查者可能在这条 PR 上添加评论,要求进行修改。如果是这样,之前的开发人员就需要转回头做出变更,然后再次发送 PR,这一步骤可能重复多次。
最终,PR 得到批准。开发人员将该分支中的所有代码合并至 master 内,具体方法通常是把分支内的全部提交内容整理成单一一条提交请求(压缩与合并)。
CI 工具会在 master 上运行一个 build。接下来,这些变更开始被并入生产环境——有时候以全自动方式进行,有时候需要人为介入其中的一个或者多个阶段。
具体工作流程可能有所不同,但相信我的描述对大多数朋友来讲都不陌生。这样的工作流程,也就是现在大家耳熟能详的 GitHub Flow。
首先我要强调一点,我知道有些团队会采用其他的分支使用方式,例如具有长期分支的 Gitflow。这些分支策略也有自己的问题。但在本篇文章中,我希望着重说明临时功能分支带来的问题,以及我为什么认为基于主干的开发方法能够很好地解决这些问题。
成功的替代方案:无需分支,基于主干的开发方法
我提出的替代方法,就是直接在 master 上工作,不再使用分支。当然,必须要以安全的方式进行。为了实现这个目标,团队需要适当采取以下关键实践:
结对编程
首先需要在团队内部普及结对编程。所谓结对编程,就是在开发人员之间结成合作伙伴,大家实时查看对方正在编写的代码,因此无需后续创建 pull 请求。事实上,结对编程能够为团队带来一系列重要的收益,包括:提高团队的适应能力、人们不必为其他杂事分心、工作完成速度更快、更高的开发质量、团队的整体编程风格更加标准、人们能够共同找到解决问题的好方法、以及知识共享等等。
在某些情况下,我们甚至可以将结对编程进一步升级为“全民编程”,即让整个团队在同一时间通过同一台计算机从事同一项工作。(例如让整个团队同时进行架构或设计决策。)
确定一套值得信赖的 build
大家还需要选定一套值得信赖的 build,其需要能够运行充分的造化测试以保证代码库处于可发布状态;其速度必须够快,以保证我们可以在代码被 push 到 master 之前完成本地构建,从而避免 push 本身对团队其他工作内容造成破坏。
为了实现这两点,团队通常需要遵循 TDD、BDD 之类的实践,同时建立起行之有效的测试策略——例如利用测试金字塔以最大程度减少慢速测试状况的出现。
如果该 build 被破坏,那么修复工作应马上成为整个团队的首要任务。如果无法快速完成修复,则应立即还原以撤销造成破坏的变更。
使用“抽象分支”或者功能标记以隐藏尚未完成的工作
在任何一个时间点上,master 当中都会包含某些尚未完成的代码。其不会造成任何危害,因为这部分代码并未被实际应用在生产路径当中。但是,将这些代码包含进 master 也有很多好处:我们可以更快注意到集成问题,可以进行更复杂的重构,也可以通过将代码对接到测试以证明代码的工作效果。
在实践当中,我发现大多数情况下,大家都可以使用“抽象分支”模式避免在生产环境中使用尚未准备就绪的代码。对于更复杂的情况,我个人会使用功能标记机制。
在之前的公司,这是一种规范化的工作方式。我在那里工作了近 6 年,而且他们早在十多年前就已经在采用这种方法。因此,我可以确定这是一种可行且能够成功推广的开发思路(文末将对这种工作方式做出详尽说明)。
基于主干开发的具体优势
我认为,以上提到的方法能够为开发团队带来诸多收益与优势,也应被视为团队需要追求的重要目标。在实际贯彻之后,我们可以直接将新代码 push 到 master,或者说跳过分支环节。以下是我亲身体会到的具体优势,顺序不分先后(当然,根据您的实际情况,大家可能会更重视其中的某些优势):
更快反馈:使用传统 PR,开发人员只能在确认代码编写完成后才能进行反馈。这个时候,我们已经无法对实质性内容做出调整。而在结对编程时,反馈会发生在代码编写当中(甚至发生在更早的问题讨论期间),因此调整工作也会容易得多。
更高的反馈质量: 使用传统 PR,反馈通常经由评论文本进行交付。除了极少数情况外,这种粗糙的表达方式很难传达软件讨论中常见的细微差别。初始作者更倾向于坚持自己的代码编写意图,而直言不讳的评论则极易引发争论甚至是冲突。在结对编程中,我们可以面对面讨论自己的想法,从而极大降低表意误解的可能性,提高讨论的效率与质量。
集体代码所有权: 当代码仅由一人编写时,此人很容易将其视为“我自己的代码”。因此才会出现“哦,这部分是 Alex 写的,你得问他”;或者“这事得等 Sam 回来了才能解决。”但在结对编程中,团队能够更轻松地建立起集体代码所有制度,并将一切产出视为“我们的代码”。
团队编码风格: 出色的团队能够编写出看似出自一人之手的软件,这意味着每位成员对团队的重视程度超过了个人偏好。结对编程时,建立这种团队风格要容易得多,而且大家也能在互相提醒当中更快培养出这种好习惯。
更频繁的集成(实际上就是持续集成): 一般来讲,只有当工作/功能完成后,才会提交 PR。通过直接将 PR 提交至 master,我们可以立即集成代码,从而实现“持续集成”中提出的“持续”这一核心原则。
提高警惕性,主动避免破坏性影响: 在直接面向 master 工作时,我们当然不希望提交任何可能产生破坏性影响的代码。这意味着我们会习惯于在 push 之前先在本地运行 build,并在一系列不断变更中完成代码实现。另一方面,分支的存在使得开发人员根本没必要太重视代码的 push 后果,这反而会建立起一种无所谓的可怕心态。
易于处理大型重构: 在使用分支时,我们往往害怕一切可能导致合并冲突的操作,例如重新命名软件包、移动代码或者架构变更。虽然在 master 上这些操作也不容易,但至少允许我们做出小幅变更,以使团队内的其他成员及时得到更新提醒。在我原先的团队中,对于那些真正棘手的变更,我们会聚焦在同一台计算机前进行快速即兴式的全民编程对话,共同研究可能的解决方案。
更好地了解每位成员的工作内容: 在分支中进行变更时,我们往往很难像直接对接 master 那样了解他人的工作内容。但相信大家都会承认,观察他人工作并主动判断他们是否需要帮助,绝对是件非常重要的事情。
更好的变更审查办法: 不同于以往审查 PR 时面对网页上的大量红线/绿线,更好的办法当然是在提前之前就对变更做出审查,这无疑是更好的处理办法。我们可以在其中使用自己熟悉的 IDE 或者任何其他偏好工具。
保留原始提交记录: 在将分支中的变更合并至 master 时,人们常常会把所有提交内容压缩为单一提交,这将丢失关于作者为何以及如何做出这些变更的相关记录。当在 master 上直接操作时,我们则能够保留每一项提交的完整记录。我曾经接手过一套十岁“高龄”的代码库,这些代码的作者早已离职,但我仍能够利用 git 历史记录准确找到对应的变更提交。参考其中的附带信息以及同期进行的其他变更,能够让我更准确地了解当时工作人员的变更意图。
基于主干开发是团队健康的重要标志
这里让我具体说明一下:基于主干开发本身并没有什么特别之处,也无法给我们带来特殊的收益。真正让我们受益的,其实是实现基于主干开发所必须具备的各种实践能力。对于任何一位能够直接在 master 上工作的团队而言,他们必须知晓如何在不破坏原有代码的情况下工作,如何编写良好的测试,以及如何高效合作等等。
可以说,基于主干的开发正是团队健康的一大标志。确实,《加速( Accelerate)》一书中提到:在对 1 万多名员工与 2000 个组织进行研究之后,他们发现基于主干开发的团队与业务水平优异的团队之间存在着强相关性。他们还发现,在高效能团队当中,分支的存在周期通常不到一天。
通过优化提升团队效能
我曾经询问不少团队,他们为什么要使用功能分支,或者是为什么不应该直接将代码提交至 master,而得到的答案通常是“我们必须确保 master 处于良好状态,master 应该随时可以发布”或者“我们需要审查代码以确保其质量符合我们的标准”等等。我对这两种观点都很认同,但在本文中,我希望向大家介绍如何在无需分支的前提下获得相同的结果。
但实际上,很多团队之所以倾向于使用功能分支,背后还有一个不为人知的理由:如果每个人都在自己的分支中工作,那么开发流程将变得更加轻松高效,因为他们不会彼此掣肘。
这样的说法没什么错误,但我仍然表示强烈反对。事实上,很多团队问题正是因此而引发。大多数团队主要针对个人效能——而非团队效能——进行优化。他们着眼于每个人的生产力,而非团队的总体生产力。用精益术语来讲,这就是一种典型的优化资源效率、而非优化流程效率的例子。
功能分支优化的是个人效能,而基于主干开发则强调优化团队效能。当针对团队效能做出优化时,个人的效率看起来反而会变慢。这是一种重要的范式转变,而且我承认有一点反直觉。
功能分支的正确用例
说了这么多,这里我还要再澄清一下:我绝对不是在否定功能分支的意义。在某些情况下,功能分支模式非常有用。根据经验,我的观点是当代码拥有明确的所有者、但其他人也需要同步协作时,功能分支是个不错的选择。
最典型的例子就是开源代码模型:在一个典型的开源项目当中,通常存在一个明确的所有者——可能是一个人,也可以是一个核心团队。与此同时,来自世界各地的贡献者会以不同的时区进行工作与交流。在这种情况下,要求贡献者发送 PR 绝对非常重要,因为所有者必须要对内容进行审查。实际上,这也正是 GitHub 发明 PR 的原因所在!如此一来,维护者能够更轻松地拒绝他们无法接受的 PR(例如某些未经讨论通过的 PR)。
在使用内部开源模式的企业中,有时也会出现类似的情况:某个团队拥有代码,但由于太过忙碌而无暇处理这些代码;另一个团队则通过发送 PR 为这部分代码做出贡献。这虽然不是解决问题的唯一方法,但有时候确实是个不错的折衷性方案。
“好吧,你说得有道理。但我该如何改变?”
如果大家目前正在使用功能分支,而且打算过渡到直接在 master 上工作的状态,那么请参考以下建议:
审查你的测试策略 并努力建立起值得信赖的稳定 build。这可能意味着大家需要进行更多 TDD、添加更多测试,同时提高 build 速度。
着手推广结对编程。如果您以往没有什么结对编程的经验,那我建议大家首先从困难的任务起步,因为这样更容易说服团队中的成员接受结对编程这种方式。接下来,大家可以在越来越多的普通任务中推广结对编程,同时培养出一拨喜爱结对方法的核心成员。团队的其他成员最终也将参与进来,毕竟谁都喜欢这种不需要代码审查的新鲜编码制度。
扩展资源
除了文章内给出的链接外,以下资源也能帮助大家更深入地理解这一主题:
这里要特别感谢 Will、Aram 以及 Pritesh 的早期反馈,也感谢他们多年来帮助我建立并完善这样一套理论。另外,也感谢 Jürgen Gmach 审阅本文草稿并提出改进建议。
附录:基于主干开发的实践方法
下面,我将向大家介绍我自己之前的团队如何实施基于主干开发以及面向 master 直接提交。相关背景与文章开关的表述基本一致:团队成员约为 6 到 10 人,大家身在同一地点;其中开发人员 4 到 6 名,测试人员 1 到 2 名,1 位业务分析师,外加 1 位团队负责人。
开发人员始终结对编程。
其中一对开发人员选择一项需要解决的新工作。作为开发筹备中的一部分,他们会编写一套或者多套验收测试框架,以及与此项工作相关的具体任务清单。
当他们认为准备就绪时,会如今团队成员并向所有人展示验收测试,以确保每位成员都对工作内容以及整个团队将要接收的成果拥有相同的理解。这一过程被称为 BDD,我们会在其中使用示例以确保理解的一致性。
每对开发人员都会从积压工作中挑选自己的下一项任务。在大多数情况下,都会有 2 到 3 对开发人员处理同一项工作,只不过各自负责不同的具体任务。
每对开发人员都会在几分钟或者几小时内提交自己的工作成果并直接 push 至 master。我们所有人都偏好这种规模有限但频率更高的提交方式,同时确保提交内容附带有明确的说明信息,例如我们为什么做出了这项特定变更。在理想情况下,提交信息应使用专业术语,同时强调我们希望通过这项提交实现的业务成果。一个典型的例子就是:“实施 acc.test 以证明每次只能将一位客户分配给一个端口”或者“我们发现其他业务部门将「room」称为「colo space」,因此需要重命名以保持名称统一。”
每当有开发人员从 git 处 pull 时,他们都会通过复位使各项提交与实际发生的顺序保持一致。
在提交之前,每对开发人员会运行一套本地 build 以确保变更成功。该 build 过程通常需要 30 秒到 2 分钟。
我们遵循测试金字塔策略,即始终偏向进行速度较快的测试,而推迟速度较慢的测试。通过使用清洁架构,我们的工作效率得到极大提升,并使我们能够将业务逻辑与技术细节隔离开来。这不是唯一的方法,但在我们的使用场景中带来了理想的效果。
利用“抽象分支”模式,确保在任何时间点上我们的代码库都能够随时发布。实际上,我们在最后一刻才把 Spring bean 添加到这个 Java 项目当中。所有新代码都可以进行测试,但在在准备好写入之后才会被真正应用于生产流程。
每当 push 代码时,CI 工具都会构建并运行所有测试。我们没有全面采用自动部署,但如果需要,这项功能可以随时实现。
针对积压工作的开发往往只需要几天就能完成。此时,我们会再次如今整个团队,展示刚刚实现的内容。
现在,将最新版本的应用程序发布到一套分段环境当中。我们的质量保证专家将在该环境中进行一系列探索性测试(通常持续数小时乃至数天)。
如果没有发生意外,则将该应用程序提升为正式版本(通常在第二天进行)。
与此同时,在上一项积压工作快要结束时,另一对开发人员会着手研究下一项积压工作,以保持良好的流动速度。我们会利用进度限制确保整个流程处于可控状态。
如果有任何成员觉得有必要如今团队讨论某些问题(例如架构决策、设计以及取舍权衡等),他们会当面通知大家。所有人都聚集在一起,参加这场即兴形式的全民编程讨论,共同提出解决方案。当大家就方案达成共识后,整个团队再次恢复为一个个结对编程小组。
原文链接:
Why I love Trunk Based Development (or pushing straight to master)
评论