在 Square Register 过去 6 年的历史中,代码库和公司都发生了显著的变化,由于应用程序已经从一个简单的刷卡终端成长为一个全功能销售终端 (point-of-sale) 系统。公司已经从 10 人发展到 1000 多人,而我们不得不迅速扩张。下面是一些我们在前进的道路上实现的流程和已经学到的东西。
随着我们的成长,我们意识到,一旦开发团队达到一定规模,按平台来组织团队会很低效。相反,我们用“全栈”团队来负责应用程序中的特定功能集。这些团队包括 iOS 工程师,Android 工程师和服务器的工程师。这给了团队更多的自由来集中精力去创造一个更深入,更全面的产品。我们围绕着餐厅,零售商店,国际化支持,硬件和核心部件 (仅举几例),组建了面向功能的团队。当团队拥有了足够的纵向所有权,就为工程师们做出更全面的技术决定留出了可能,并让他们拥有了对产品确定的归属感。
2014 年之前,Register 的发布还遵循瀑布模型;我们规划一个大的功能集,然后设定一个未来的最终日期 (三至六个月),然后努力实现这些功能。
这个流程没法很好地扩展。当我们为产品增加功能和工程师时瀑布模型变得费力和缓慢。由于发布版本的所有开发功能必须一起发布,某一个功能的延迟或有问题会耽误整个发布。为了确保团队持续保持自主性,我们找到了一个不同的,更有效的方法。
为了保持高效率,我们总是希望确保我们的流程符合我们的规模。从 2014 年开始,我们引进了一个由 “发布列车” 组成的新模式。发布列车优化了功能团队的自主权,同时支持持续发布。这意味着单个功能可以在它们准备好的时候就被发布,而不必等待其他工作的完成。
切换到发布列车需要改变我们的工作流程:
这意味着,我们的主分支保持在一个稳定的状态。这就是所说的列车的一部分。
这有很多的好处:
在 2015 年年初,我们对这个流程做了进一步改进:现在发布分支被按两个星期的时间为间隔分割和发布。这意味着团队在今年将有 26 次发布的机会。相比 2013 年及更早的每一年只有三个或四个发布版本而言,这是一个巨大的胜利。更多的发布机会意味着交付给客户更多的功能。
Square 的商人依靠 Register 来经营生意。因此,它在任何时候都必须是可靠的。我们有严格的流程,以确保设计、实装和测试阶段的质量。
“写下来是让你知道你的思维有多草率的最自然的方式” - Guindon
这是我最喜欢的一句话之一,而且它也适用于软件构建!如果你只是在你的头脑里构建软件的话,该软件将是有缺陷的。在你脑袋里的形象是很模糊和短暂的;它总是持续变化的,因此需要写下来加以澄清和完善。
Square 里每一个大的变化,都要经过工程设计审查。如果你以前从来没有做过,这听起来会有点吓人,但它其实很简单!这个过程通常需要编写以下的设计文档:
然后,我们会有两到四个评审来审查文档,提出问题,并作出最后的决定。这些评审应该熟悉你要扩展的系统。
这似乎是大量的工作,但它是值得的。最终的结果将是一个更茁壮和更易理解的设计。我们不断地看到,当一个变化经过了设计审查,会使得错误更少,并降低了复杂性。另外,作为一个副产品,我们还得到了经过评审的系统文档。棒!
因为以下几个原因,我们的代码审查过程是很严谨的:
我们的 pull requests 流程是什么呢?每次 PR 必须满足:
同样,评审们被要求:
在合并之前,所有的测试必须通过。单元测试和我们的自动化集成测试 (使用 KIF) 跑成功前,pull request 的合并都是被禁止的。
我们已经开始在做下面的事情来帮助简化和加快 Register 的开发过程。
在 Register 团队的成长中我们学到的有一件事是“口头说说”是很糟糕的知识传递方式。如果一年只有几个工程师加入项目的话,这不会是一个问题,但如果一个月就会有几个工程师加入,尤其是如果他们只是针对临时项目 (例如临时需要一个服务器工程师帮助建立一个特别的功能),这种尺度很快会变得很费时。一个具有标准和团队实践的保持更新的文档就变得很重要。这份文档应该包括哪些内容呢?
你可能会注意到这里的一个规律:凡是能在 10 分钟以内回答的问题都应清楚地记录下来。
需要几个工程师花数分钟的手动流程如果用更多工程师可能需要花更长的时间。任何时候你看到琐碎的东西花费了大量的时间,都应该尽可能让它自动化。
我们最近的一个最大的“自动化”成功案例是我们的 Objective-C 代码风格指南:我们现在使用 clang-format 来自动格式化提交到 Register 及其子模块的所有代码。这消除了代码审查里面的各种“没有换行”或者“太多的空白”的意见,这意味着审阅者可以专注于真正提高产品质量的东西。
我们每天都要合并很多的 pull requests。之前这些“挑剔风格”的意见会在每个 pull request 额外花 10-20 分钟 (鉴于审查者和作者)。这意味着仅是风格指南的自动化都让我们每天节省了两小时或更长时间。也就是一个星期 10 个小时。增加太快了!
另一个自动化节省时间的例子是我们每天都会发送 “Pull Request 状态”的邮件。
在这个电子邮件存在之前,每天早晨我们当中的 10 到 15 个人会挤在一个桌前站 10 分钟,分配 pull requests 的审查。而现在,我们每天早上发出一个包含了所有开放 PR 列表的电子邮件,以及谁被分配来对其进行审查。不需要再额外开会了。这意味着我们又多了每天 2 个多小时或每周 10 小时的开发时间。
这个每天 PR 状态电子邮件的另一个好处是,我们可以轻松地跟踪评审发生了什么事:花了多长时间,哪个工程师贡献最大,哪个工程师审查了最多。这有助于揭示那些可能拖累团队的时间分配问题 (比如是否一名工程师做了团队一半的评审?)。
如果你的 bug 被分散在多个跟踪器上是不可能发布无缺陷的产品的。有一个地方可以让我们看到一切有关当前版本的信息是极其重要的:bug 的数量,每个工程师未解 bug 的数量 (是否有谁忙不过来了?),以及 bug 的总体趋势 (我们修复它们速度比它们被创建的速度更快吗?)。
如果只有几个工程师在做同一个项目,可以很容易地保证质量:因为所有的工程师都清楚了解代码库,他们也都有强烈的归属感。但当一个团队扩展到 5、10、20 或更多的工程师的时候,维护这样的品质变得更加困难。重要的是要确保每个组件和功能有明确的负责维护它质量的所有者。
在 Register,我们最近决定应用程序的每个逻辑组件都要有明确的所有者。这些所有者都记录在一个列表里以便查找。什么是一个组件?它可能是一个框架,它可能是一个面向客户的功能,它也可能是两者的某种组合。确切的分界并不重要;最重要的是确保应用程序的每一行代码都是有人所有的。这些所有者要做些什么呢?
在指派出组件明确所有者后,我们得到了很好的结果:有明确所有者的组件和默认所有人为所有者的组件相比,代码质量是持续增高的 (bug 率也较低)。
这是我们最近的另一项改变:我们已经开始在主分支上严格执行“不回退”的规则。这样做的好处是什么?我们的主分支现在一直都很稳定。如果有人发现了一个错误,他完全不需要去想是否需要提交这个报告。这么做也能减少 QA 的负担,因为花在搞清楚问题是否应提交或者它们是否重复上的时间少了。如果发现了错误,就提 bug。
这一策略和发布列车模型是齐头并进的:几乎在任何时候,我们都可以从主分支拉一个发布分支,并在短短几天内发布到 App Store。这对一个像 Register 这样的一个大型应用程序是非常有价值的,它可以帮助我们尽可能快的做出行动。
鉴于我们的规模,保持主分支在可发布状态,也有助于避免“破窗效应(broken windows)”的问题;发现的时候就修正 bug,确保工程师让自己保持更高的标准。
确保 Register 里的每一个组件在建造和设计的时候都保持了可测试性的初衷是非常重要的。没有这一点,我们就需要成倍扩大手动 QA 的工作量:两个功能可以通过四种方式进行交互,三个功能可以在八个方面互动,等等。显然,这是不合理的、不可靠的,也是不可扩展的。
当我们在为某个功能做工程设计工作的时候,我们不断地问自己:“这个可以测试吗?我在让自动化测试容易进行吗?“
建立可测试性也有一个额外的好处:它引入了所有 API 的二次使用 (即测试本身)。这意味着工程师们不得不花更多的时间思考一个 API 的设计,确保它在多个情况下都是工作的。其结果是,这将使得其他工程师重用 API 变得更容易,节省了未来的时间。
对我们来说,测试不是可选项,而是一个需求。如果你在 Register 提交代码,必须包括测试。
想想看:如果一个开发团队有 365 个工程师,每位工程师只需要每年弄坏一次主分支,就可以让项目在整一年都停摆了。这显然是不能接受的,并且会极大的减慢进度和挫败的开发团队。
有什么简单的方法来防止主分支被破坏?首先当然是不合并错误的代码!这就是 pull request CI 需要做的,每个 Register 的 pull request 都有一个 CI 在有新提交的时候被触发。大约 15 分钟后,工程师就可以放心的提交 PR,因为他或她不会因此引入任何导致回退的问题。
当我们有新加入的工程师时,这会是非常有价值的。他们可以提交代码,而无需担心他们将引入让主分支不工作的改动。
以下是在过去三年当 Register 的 iOS 团队不断壮大的一些个人看法。
在一个大的应用程序里,你会有大量的代码。有些代码是很老的了。但老并不一定意味着坏。只要你有良好的测试覆盖率,旧代码将继续正常工作。不要把时间花在“清理”那些的履行了需求并且没有拖累任何人的代码上去。这种清理过程中你能做的最好的事情就是不破坏任何东西。所以还是把这些时间花在创建新的功能上吧。
在一个大的代码库里,你很容易就把所有的时间都花在其中,而无法从外界学习新东西。
你怎么解决这个问题?每周花些时间 (我每天预留一小时) 从你的代码库之外的资源来进行学习。你能从哪儿学习呢?可以看看那些听起来有趣的讨论,或者阅读你觉得有兴趣的领域的文章。坚持这样做,你会发现这些并行的知识会为你的日常工作带来很多好处。有时,正是这些小事情会造成结果的巨大区别。
很少有可以立即解决的事情,包括技术累积。如果技术累积需要很长的时间,不要让自己感到沮丧,尤其是在一个大的代码库里。
想想像体重增加一样积累技术:你并不会一夜就增加一百磅;它是逐步显现的。就像减肥一样,也需要大量的时间和精力来消化技术 - 从来都不会有一个瞬时方案。在累积的同时跟踪你的进步,并确保它在一个合理的速度向下进展。
如果你有任何问题,请随时通过 k@squareup.com 联系我。感谢您的阅读!
(感谢 Connor Cimowsky, Charles Nicholson, Shuvo Chatterjee, Ben Adida, Laurie Voss, and Michael White 的审查。)