Square Register 中的扩张
Square Register 的扩张
在 Square Register 过去 6 年的历史中,代码库和公司都发生了显著的变化,由于应用程序已经从一个简单的刷卡终端成长为一个全功能销售终端 (point-of-sale) 系统。公司已经从 10 人发展到 1000 多人,而我们不得不迅速扩张。下面是一些我们在前进的道路上实现的流程和已经学到的东西。
团队
随着我们的成长,我们意识到,一旦开发团队达到一定规模,按平台来组织团队会很低效。相反,我们用“全栈”团队来负责应用程序中的特定功能集。这些团队包括 iOS 工程师,Android 工程师和服务器的工程师。这给了团队更多的自由来集中精力去创造一个更深入,更全面的产品。我们围绕着餐厅,零售商店,国际化支持,硬件和核心部件 (仅举几例),组建了面向功能的团队。当团队拥有了足够的纵向所有权,就为工程师们做出更全面的技术决定留出了可能,并让他们拥有了对产品确定的归属感。
我们的发布流程
2014 年之前,Register 的发布还遵循瀑布模型;我们规划一个大的功能集,然后设定一个未来的最终日期 (三至六个月),然后努力实现这些功能。
这个流程没法很好地扩展。当我们为产品增加功能和工程师时瀑布模型变得费力和缓慢。由于发布版本的所有开发功能必须一起发布,某一个功能的延迟或有问题会耽误整个发布。为了确保团队持续保持自主性,我们找到了一个不同的,更有效的方法。
都登上发布的列车
为了保持高效率,我们总是希望确保我们的流程符合我们的规模。从 2014 年开始,我们引进了一个由 “发布列车” 组成的新模式。发布列车优化了功能团队的自主权,同时支持持续发布。这意味着单个功能可以在它们准备好的时候就被发布,而不必等待其他工作的完成。
切换到发布列车需要改变我们的工作流程:
- 增量开发 - 功能是逐步发展的,而不是长期的被隔离的功能分支。
- 隔离和安全 - 新功能后面都有一个服务器控制的标志。该功能标志在开发环境之外是不被启用的,直到准备发布之时。
- 无回退 - 如果某个改变在现有的功能上导致了回退,那这个回退必须被立即修复。
- 制定一个时限 - 团队在一个发布期限内可以包含两到三个功能而不是在某个特定版本仅发布一个功能。
这意味着,我们的主分支保持在一个稳定的状态。这就是所说的列车的一部分。
- 分支 - 在每个月的月初,会从主分支创建一个发布分支。
- 上车 - 如果一个功能准备发布 (基本已经没有问题),它的功能标志被启用。如果不是,它必须等待下一班列车。
- 测试和修复 - 每个月的其余时间花费在对发布分支的错误修正上。如果一个团队不需要发布任何东西,它会继续在主分支上工作。
- 合并 - 发布分支上的变化会不断合并回主分支。
- 发布 - 在月底我们会把发布分支发布到 App Store。
- 重复 - 之后的每月重复上述过程。
这有很多的好处:
- 每个版本之间不会有超过一个月的代码变更,导致错误更少。
- 只有 bug 修复会进入列车的发布分支。这意味着更长的“烘焙时间”来证明改变是稳定的。
- 发布 bug 修复版本的需求减少了;因为大多数 bug 的修复都可以等到下一次列车。
- 通过逐步建立和合并功能,避免了可能会破坏主分支的破坏性大合并。
- 主分支是不接受回退或高优先级的错误的。修复这些是团队的最高优先级。
- 在某个具体日期发布的压力减少了。下一个版本不必等待数月,功能可以在下个月的发布中就包含进去。这意味着团队不需要急着赶工。他们只需在他们对功能的代码质量有信心的时候发布。这提高了团队的工作效率,士气和代码质量。
在 2015 年年初,我们对这个流程做了进一步改进:现在发布分支被按两个星期的时间为间隔分割和发布。这意味着团队在今年将有 26 次发布的机会。相比 2013 年及更早的每一年只有三个或四个发布版本而言,这是一个巨大的胜利。更多的发布机会意味着交付给客户更多的功能。
我们的开发进程
Square 的商人依靠 Register 来经营生意。因此,它在任何时候都必须是可靠的。我们有严格的流程,以确保设计、实装和测试阶段的质量。
大型工程变更需要设计和评审
“写下来是让你知道你的思维有多草率的最自然的方式” - Guindon
这是我最喜欢的一句话之一,而且它也适用于软件构建!如果你只是在你的头脑里构建软件的话,该软件将是有缺陷的。在你脑袋里的形象是很模糊和短暂的;它总是持续变化的,因此需要写下来加以澄清和完善。
Square 里每一个大的变化,都要经过工程设计审查。如果你以前从来没有做过,这听起来会有点吓人,但它其实很简单!这个过程通常需要编写以下的设计文档:
- 目标 - 你想实现什么?这个变化对客户有什么影响?
- 非目标 - 你不打算去实现的什么?有什么界限?
- 度量 - 你将如何衡量成功或失败?
- 调查 - 你还调查过其他的什么解决方案 (如果有的话)?你为什么没有选择它们呢?
- 选择 - 你决定了要做什么?你为什么决定这样做?
- 设计 - 你所选择的设计有些什么细节?包括 API 概述,技术细节,以及 (可能的) 一些头文件例子,再配上其他任何你认为有用的东西。这是你把你的设计卖给你自己和你的同伴工程师们的地方。
然后,我们会有两到四个评审来审查文档,提出问题,并作出最后的决定。这些评审应该熟悉你要扩展的系统。
这似乎是大量的工作,但它是值得的。最终的结果将是一个更茁壮和更易理解的设计。我们不断地看到,当一个变化经过了设计审查,会使得错误更少,并降低了复杂性。另外,作为一个副产品,我们还得到了经过评审的系统文档。棒!
我们的代码审查过程
因为以下几个原因,我们的代码审查过程是很严谨的:
- App Store 的时限 - 如果我们发布了一个 bug,由于 App Store 的审查过程会使得修复延迟约一个星期给客户。
- Register 是至关重要的 - 查找错误很重要,因为 Register 对餐厅、零售商店这些用户来说非常关键。
- 大型应用程序 - 在一个像 Register 这样大的应用程序里在合并后再去找 bug 是很困难的。
我们的 pull requests 流程是什么呢?每次 PR 必须满足:
- 被追踪了的 - 每一个 PR 都有对应的 JIRA issue。
- 被描述了的 - 每一个变化后面都必须有是什么以及为什么的一个清晰的描述。
- 是可以被评审的 - Pull request 的 diff 文件必须是 500 行以内。大型的 diff 是不允许的。如果一个 diff 比 500 行多很多的话,评审很容易看不到错误。
- 集中 - 不要把重构或重命名跟一个新的功能放到一起做。把它们分别开来。
- 自我审查 - 在增加其他评审之前,所有 PR 的作者都要求对改动先做自我审查。这是为了避免杂散的 NSLog,缺失的测试,不完整的实现方式之类的事情。
- 有两个指定的评审通过 - 评审员中的一个必须是被改变组件的所有者。我们需要明确的列出评审人员,以确保工程师们知道在他们的审核队列到底有些什么。
- 被测试了的 - 包括了改动的稳定性和正确性测试。没有测试的 pull request 会被拒绝。
同样,评审们被要求:
- 清楚 - 意见必须清晰,简明。对于新的工程师,评审应包括可遵循的例子。
- 解释 - 不要只说 “把 X 改成 Y”;也要解释了为什么应该这样改。
- 不要挑剔代码风格 - 这是自动格式化干的事 (接下来会讲)。
- 文档 - 每个代码审查意见必须被标记为下列之一: — 必须 (“必须在合并前修复。”) — 意见 (“迟早都需要修复。”) — 个人喜好 (“我会这样做,但你不必。”) — 问题 (“这是干嘛的?”)
- 乐于帮助 - 评审必须有一个乐于帮忙的心态来进行代码审查。评审的工作是帮助代码安全地合并,而不是阻止它合并。
在合并之前,所有的测试必须通过。单元测试和我们的自动化集成测试 (使用 KIF) 跑成功前,pull request 的合并都是被禁止的。
一些流程 Tips
我们已经开始在做下面的事情来帮助简化和加快 Register 的开发过程。
尽可能地为通用流程建立文档
在 Register 团队的成长中我们学到的有一件事是“口头说说”是很糟糕的知识传递方式。如果一年只有几个工程师加入项目的话,这不会是一个问题,但如果一个月就会有几个工程师加入,尤其是如果他们只是针对临时项目 (例如临时需要一个服务器工程师帮助建立一个特别的功能),这种尺度很快会变得很费时。一个具有标准和团队实践的保持更新的文档就变得很重要。这份文档应该包括哪些内容呢?
- 代码审查指南 (对于提交者和审查者) — “我需要多少个评审人?我什么时候可以合并?“
- 设计审查指南 — “我应该如何设计这个功能?”
- 测试指南 — “我该如何测试呢?我们使用什么测试框架?”
- 模块/组件所有者 — “我可以跟谁讨论 X?谁建的呢?”
- 问题跟踪指南 — “我在哪里可以查找并跟踪我必须做的事?”
你可能会注意到这里的一个规律:凡是能在 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 的数量 (是否有谁忙不过来了?),以及 bug 的总体趋势 (我们修复它们速度比它们被创建的速度更快吗?)。
保持共享代码库的质量
如果只有几个工程师在做同一个项目,可以很容易地保证质量:因为所有的工程师都清楚了解代码库,他们也都有强烈的归属感。但当一个团队扩展到 5、10、20 或更多的工程师的时候,维护这样的品质变得更加困难。重要的是要确保每个组件和功能有明确的负责维护它质量的所有者。
每一个组件都需要一个所有者
在 Register,我们最近决定应用程序的每个逻辑组件都要有明确的所有者。这些所有者都记录在一个列表里以便查找。什么是一个组件?它可能是一个框架,它可能是一个面向客户的功能,它也可能是两者的某种组合。确切的分界并不重要;最重要的是确保应用程序的每一行代码都是有人所有的。这些所有者要做些什么呢?
- 他们审查和批准代码更改和文档设计。
- 他们了解“关键部位”,以及如何解决它们。
- 他们可以为新来的工程师提供组件的概述。
- 他们确保质量始终越来越好。
在指派出组件明确所有者后,我们得到了很好的结果:有明确所有者的组件和默认所有人为所有者的组件相比,代码质量是持续增高的 (bug 率也较低)。
保持主分支是可发布的
这是我们最近的另一项改变:我们已经开始在主分支上严格执行“不回退”的规则。这样做的好处是什么?我们的主分支现在一直都很稳定。如果有人发现了一个错误,他完全不需要去想是否需要提交这个报告。这么做也能减少 QA 的负担,因为花在搞清楚问题是否应提交或者它们是否重复上的时间少了。如果发现了错误,就提 bug。
这一策略和发布列车模型是齐头并进的:几乎在任何时候,我们都可以从主分支拉一个发布分支,并在短短几天内发布到 App Store。这对一个像 Register 这样的一个大型应用程序是非常有价值的,它可以帮助我们尽可能快的做出行动。
鉴于我们的规模,保持主分支在可发布状态,也有助于避免“破窗效应(broken windows)”的问题;发现的时候就修正 bug,确保工程师让自己保持更高的标准。
从开始就建立可测性
确保 Register 里的每一个组件在建造和设计的时候都保持了可测试性的初衷是非常重要的。没有这一点,我们就需要成倍扩大手动 QA 的工作量:两个功能可以通过四种方式进行交互,三个功能可以在八个方面互动,等等。显然,这是不合理的、不可靠的,也是不可扩展的。
当我们在为某个功能做工程设计工作的时候,我们不断地问自己:“这个可以测试吗?我在让自动化测试容易进行吗?“
建立可测试性也有一个额外的好处:它引入了所有 API 的二次使用 (即测试本身)。这意味着工程师们不得不花更多的时间思考一个 API 的设计,确保它在多个情况下都是工作的。其结果是,这将使得其他工程师重用 API 变得更容易,节省了未来的时间。
对我们来说,测试不是可选项,而是一个需求。如果你在 Register 提交代码,必须包括测试。
CI 对 Pull Requests 的重要性
想想看:如果一个开发团队有 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 的审查。)