软件工程生命周期:我们如何构建新的Dropbox Plus

2020-09-17 03:46:34

几周前,我们为个人用户付费套餐Dropbox Plus发布了一系列新功能。虽然我们最初只是一家存储公司,但我们已经成长为管理您的数字生活的中心。大约有150人参与了这次发布:工程师、产品经理、设计师、文案等等。

由于运气和偶然性的结合,我有幸接触到了这次发射的几乎每一个部分。我看到了不同的团队是如何工作的,因为我在软件工程生命周期的不同阶段加入了每个团队。

在这篇文章中,我将提炼我在发布的不同部分的工作经验,并讨论我们是如何考虑构建软件的。我将深入讨论一些技术细节,但主要集中在团队如何组织和操作。

接近2019年底,我们意识到:虽然我们已经为我们的专业用户打造了令人难以置信的新产品,如新的Dropbox和Dropbox Transfer,但我们已经有一段时间没有为我们的个人用户提供新的价值了。

我们开始着手改变这一点。我们组建了一个团队,专门致力于帮助个人用户管理他们的数字生活。当时,我们不知道这些生活和需求会在未来一年里发生多大的变化,但事后看来,我们的时机是正确的。

我们与客户的合同很简单:我们生产人们非常喜欢的产品,以至于数百万人为此付费。一般来说,我们渴望在任何情况下都值得信任。你是我们的客户,不是我们的产品。

融合了我们的使命和价值观,我们想出了一个简单的计划:向我们最忠实的用户发送一系列及时、有用的功能。在对构建什么进行了大量讨论之后,我们提出了新的Dropbox Plus。

我们知道我们的用户需要计算机备份。这终于成为可能,这要归功于我们全新的同步引擎,我们刚刚在6月份完成了这一引擎的推出。

我们做了几周的客户调查,了解到用户希望从我们这里获得一些特定的新功能。他们想要一个特殊的文件夹来存储他们最重要的文件,这就是Dropbox Vault。

我们的研究还发现,我们的大多数用户仍然没有使用密码管理器。我们知道这将是他们数字安全的基础,所以我们收购了Valt,并将他们的产品集成到Dropbox产品套件中,以满足我们用户的需求。具有讽刺意味的是,我们在收购一家名为Valt的公司时,正在构建一个名为Vault的安全文件夹。

最后,用户表达了与家人分享他们的Dropbox计划的愿望,而不是共享他们的账户。所以我们想出了Dropbox家庭计划。

从2020年开始,我就是家庭团队的一员。我们的任务是为用户建立一个新的付费计划,让他们在一个订阅下与家庭成员共享我们所有的新功能和现有功能,就像他们在家里使用其他订阅一样。

这个团队是全新的,在产品规格还在制定的时候就成立了。我们还没有写任何代码。我们首先要回答很多问题:用户想要一个家庭文件夹还是共享配额?一个家庭可以有几口人?家庭与工作场所的“团队”有什么不同?

在开始实施之前,我们还在架构决策上投入了大量时间,以设计出既能满足当前需求又能适应未来增长的产品。当开始一个新的项目时,列出一个粗略的计划是很重要的,即使你知道这些计划稍后会随着你学到的更多和理论与实践的结合而改变。

我们的数据模型应该是什么样子?我们想要如何编写API?我们如何与我们现有的支付系统整合?一旦开始编码,提前计划可以避免浪费开发人员周期。

早些时候,我们意识到我们不应该在团队现有的Dropbox基础设施上构建这个产品。我们现有的团队模型是为专业用例构建的,涉及家庭用户不需要的大量高级共享、权限和用户管理功能。这会拖慢我们为计划生育改造这一系统的速度,在某些情况下,还会迫使家庭进行令人厌恶的抽象。例如,团队管理员可以访问任何团队成员的文件-这是许多家庭都不想要的。

当我们敲定确切的需求时,我们开始为该项目规划技术基础设施。这是在一月份,当时我们还在实体办公室工作。核心家庭团队的所有工程师都在纽约办公室。

工程团队的每个成员都带来了一套不同的专业知识。我亲自负责领导前端架构。我的工作是弄清楚如何建立所有的家庭管理和邀请页面。其他人则专注于API、数据模型或处理共享配额。

我们一起坐在一个吊舱里,不断地互相交流想法。因为这是一个新项目,而不是扩展现有的体系结构,所以我们能够在设计数据模型、API和用户界面时都考虑到其他因素。团队经常坐在彼此的办公桌前,讨论如何对我们正在解决的问题进行建模。有了全天的密切联系,每个人对每件事都有了足够的背景信息。

我特别自豪的是,我们决定将所有页面构建为独立的应用程序。例如,我们知道家庭管理页面很可能最终成为帐户设置的一部分,但我们不想被该开发过程所拖累。直接建立在已经建立的页面帐户设置页面上会减慢我们的速度。

原因如下:要在Account页面上测试,我们必须创建一个Plus用户,导航到Account页面,并邀请成员加入Family,然后才能开始测试页面上现有的核心功能。这意味着需要2-3分钟的编辑/刷新周期,这在我们的快速迭代阶段是完全不可接受的。我们需要能够在几秒钟内进行测试,因为我们仍在弄清楚我们将提供的确切体验。

相反,我们使用一组测试夹具启动了一个开发人员沙箱。我们在顶部嵌入了根Family管理组件。这使得我们可以单独测试Family设置页面,并且只关心正在开发的部件。

作为这项工作的一部分,我们投资了一项名为API-QL的新内部技术。我们使用内部接口描述语言为我们的API构建REST端点。这提供了客户端/服务器类型验证等方便的功能,但不提供缓存、轮询或反应挂钩。POLLO是一种流行的GraphQL客户端,它提供了所有这些功能,允许我们在不更改服务堆栈的情况下尝试GraphQL。

API-QL是Apollo和这些REST端点之间的一个层。它构建在Apollo的本地解析器之上,并在客户端实现了一个轻量级的GraphQL服务器。API-QL允许我们构建REST端点,但仍然可以利用Apollo提供的缓存和其他开发者体验。凭借API-QL和协作文化,我们所有的API在设计时都考虑到了API-QL。

对于每个新功能,我们都为项目设置了一系列里程碑。在家庭计划的情况下,我们关注以下几个方面:内部alpha、外部alpha、beta,最后是GA(全面可用性)。我是通过内部阿尔法加入团队的,所以我们在这里讨论这个。附录A包含所有剩余里程碑及其目标的列表。

内部Alpha的目标是将一些东西发送给其他Dropboxers,这样我们就可以对该功能进行狗粮处理。我们努力为每个功能都这样做,这样我们就可以测试基本功能,并确保所有管道正常工作。这有助于我们在发货给任何外部用户之前建立对产品质量的信心。

我们决心不断地向用户传递价值,无论这些用户是Dropbox的员工、朋友和家人,还是好奇的测试者。为了达到第一个内部阿尔法,我们不断缩小范围。我们的目标是尽快为我们的内部阿尔法计划提供最低限度的可行产品,而不包括任何可能给他们带来麻烦的东西。它需要端到端地工作。它不需要打磨。

因此,任何时候出现模态都会问“您确定要这样做吗?”我们将该特定功能重新安排到稍后的里程碑。

我们还推迟了一些不错的功能,比如联系建议或电子邮件提醒。这些功能对于成品是必要的,但会减慢我们的速度,而且不是新的家庭计划所独有的,这也是我们希望我们的alpha用户测试的。

本季度中期,我们有一位非常有经验的前端工程师加入了Dropbox和Family团队。作为前端负责人,我有责任提出一个范围很广的项目,帮助他们了解我们的堆栈。

我们基于里程碑的计划在这里真的很有帮助。我们已经决定需要最终产品的联系建议,但没有将其作为内部alpha的一部分,因为我们可以使用一个简单的文本字段。这是一个完美的入门项目:它触及了我们堆栈的每一层,但直到后来的里程碑才是必要的。我们的新员工可以沉浸在我们的团队中,而不会感觉到他们在拖累我们其他人。

当他们加大力度时,事情变得很明显,他可以从我手中接过计划生育的前台。这是及时的,因为家庭之外的另一种产品需要一些关注。

在3月份的第一周,我和我的经理在我们每周一对一的讨论中讨论了Vault团队需要有人来制定前端技术方向。该团队的任务是为用户最重要的文档建立一个安全的文件夹。他们的大部分努力都集中在尽可能确保该文件夹的安全上。

他们在不到9周的时间里制造出了最低限度的可行产品。现在我们需要把它塑造成一个稳定的基础。我们为我制定了一个过渡计划,让我在3月的最后一周交出我在“家庭”(Family)上的所有工作,同时它还在Alpha中,并在4月的第一周开始在Vault上加紧工作。

不久之后,2020年到来了。3月13日,Dropbox宣布,由于新冠肺炎的缘故,我们都将在家工作。当时我们以为就几个星期,我们都太天真了。在我意识到这一点之前,我在家庭团队的时间已经结束了,我们还在家里。我必须弄清楚如何在大流行期间加入一个新的团队。

我做的第一件事就是与我将要共事的每个人建立了1:1的关系。我们有工程师负责锁定/解锁文件夹、提供API、更新桌面应用程序,以及处理现有的前端。我们还有一位产品经理和一位设计师。我知道,如果我要成为团队的一名有效成员,尽早建立这些关系是很重要的。

一旦公司把我们都送到家里工作,我们就对Zoom进行了1:1的谈判,而不是等到4月份。这对每个人来说都是一个转变,但它奏效了。随着时间的推移,我弄清楚了我将与谁一起工作最多。我和他们每周安排一次1:1的会议,以确保我们在遥远的地方保持同步。

在团队中呆了几周后,我完成了自己的入门项目-更新用户入门-并开始专注于代码库质量。我开始着手处理在前端设置技术方向的任务。我们现有的前端代码很难测试。它的意大利面比我们想要的要多得多,因为它建造得太快了。我们希望迁移到更强大的基础,而不会减慢团队的进度。

为了实现这一点,我开始编写一个名为“Vault前端:在正确方向上重构”(Vault前端:Refactoring in the Right Direction)的文档。本文档的节略版本可在本帖子底部的附录B中找到。

为团队代码定义一套可靠的标准是一回事。但是为了在产品中发布这些标准,我需要得到团队其他成员的认可。我与团队中的另外两名前台工程师分享了初稿。然后我们三个人就把它塞进去,直到我们都对它的规格感到满意。

除了弄清楚团队应该做什么之外,我们还必须弄清楚为什么。当然,遵循最新的最佳实践编写代码会更有趣。但团队也应该理解一些重要的商业胜利。更高质量的代码更容易阅读、更容易更新和更容易维护。我们的可测量目标是减少任何给定星期传入的bug数量。

我们三个人向团队提交了这份文件,每个人都同意。随着时间的推移,我们对代码库的状态越来越满意。

然而,我们注意到,我们做出的一个决定正在拖慢我们的速度。

在我的建议下,团队已经开始使用API-QL进行API调用。虽然这提高了家庭团队的产量,但对于跳马团队来说,这被证明是一个错误。Family团队在绿地代码库中操作,并在构建API时考虑到API-QL,在Vault上,我们将API-QL升级到现有API上。数据模型没有对齐,这减慢了可信联系人功能的开发。

这里有一个重要的教训:在一个项目上完美工作的想法可能不会转移到另一个项目上。API-QL是一项非常棒的技术,我们将继续对其进行投资,但目前它并不适合这个项目。

总体而言,我们的标准文档为我们的代码前进提供了坚实的基础。虽然API-QL并不合适,但我们能够修改文档并继续朝着正确的方向重构。这减少了传入的错误数量,使我们的代码更易于使用,并且总体上加快了开发速度。

最重要的是,它让我们所有人都站在了同一条战线上。不仅是文档,而且共同编写文档的过程也帮助我们定义了好的代码在我们看来是什么样子。这使得代码审查更快、更一致,因为我们都有一个共享的参考框架。

当我完成Vault的前端技术指导时,我被拉进了一个相关的项目:新功能的入口点。仅有诸如保险库、密码和计算机备份等功能是不够的。需要给用户提供找到它们的方法。我们不能指望他们去找。我们需要在他们所在的地方与他们会面。

这听起来可能很简单,但这是一项涉及产品、平台和基础设施团队的大型协作工作。我们的工作是深入研究所有其他团队的代码库,并将我们的新功能与他们现有的源代码进行集成。我们已经有两名工程师全职从事这项工作,但其中一人即将休陪产假。

我加入这个项目是为了接手前台工作。我们有大约两周的过渡期,我们三个人都在一起工作。然后就只剩下我们两个人了--我和迈克尔。我负责前台,他负责后端。

这项工作带来了自身的挑战。在Family上,我们是白手起家的,但在Vault上,我加入了一个运行中的产品。现在,为了向用户呈现入口点,我们需要在整个代码库中做大量的微小更改。然而,虽然Family和Vault是各自由大约10名开发人员组成的团队,但入口点只有Michael和我。

在两个月的时间里,我们推出了门票,将我们的新功能与Dropbox现有的表面进行了整合。我们不得不深入研究从web用户界面到同步引擎的方方面面。与需要关注架构或代码质量的Family或Vault不同,我们触及的是不属于我们的代码,我们的关注点是全力以赴地“完成它”。

在一个特别令人兴奋的场合,我们与SYNC团队的一名工程师进行了半天的配对。当用户删除他们的一个文件系统入口点时,我们需要显示警告。我和Michael都不熟悉同步引擎使用的语言Rust,所以我们需要引进一天的专业知识。

这听起来压力很大,但这次经历真是令人振奋。用一门你不懂的语言进行结对编程感觉就像拥有超能力。我也很高兴我可以声称我接触了我们的新备份功能,即使我只添加了几行代码。我们把这个更改放到客户机上,然后屏息等待它发布给用户。

不知不觉中,我们新改进的Dropbox Plus开始投放市场。这一过程涉及到随着时间的推移将功能扩展到我们的用户子集。我们有一种名为Stormcrow的内部门控技术,它允许我们设置我们想要接收的人口的百分比。我们向1%的用户启用了所有新功能,然后是10%,然后是25%,然后是50%,然后是100%。

我希望我能说这件事进展顺利,但事实并非如此。

在发出公告电子邮件后,我们意识到有如此多的用户试图立即注册Plus,以至于我们的支付系统跟不上。我们回滚了这些功能,修复了支付系统,然后又重新打开了这些功能。

在解决了这个问题并调整了我们的电子邮件节奏以记住流量之后,这些功能对每个人都是实时的。在短短几周内,数以百万计的用户试用了我们的新功能。

对于每个新功能,我们都为项目设置了一系列里程碑。在家庭计划的情况下,我们关注以下几个方面:内部alpha、外部alpha、beta,最后是GA(全面可用性)。

内部阿尔法是我们的第一个里程碑,这里的目标是“测试管道”。我们希望Dropboxers升级,邀请家人,并测试我们的表面。这一里程碑使我们能够在生产中使用真实用户进行测试,即使他们都是Dropboxers及其家人。上面有更多详细信息。

外部阿尔法是为了从更大、更多样化的真实用户群中获得数据,以便为我们提供信息,并使我们做出关于产品的决策。这意味着要走出Dropboxers及其家人的圈子。我们想看看用户多久升级一次,他们增加了多少成员到他们的家庭中,并从外部世界收集一般反馈。

外部alpha是收集信号以帮助最终确定产品决策,而beta则是确保我们定义和构建的最终产品的稳定性。我们在测试版中的目标是尽可能多地删除未知的未知因素,并找出真正的用户会遇到什么错误。

最后,GA指的是我们去掉测试版标签,向所有用户发布完整的产品。在这一点上,我们对我们所做的产品决策和软件本身的质量都很有信心。

当我们在Vault前端工作时,我们应该朝着正确的方向重构我们的代码。所有未来的不同之处都应该着眼于纠正违反这些规则的先前存在的代码。

不要阻碍进度,我们团队的任务是提供一款用于存储您最敏感文件的产品。可维护的代码库是实现该目的的手段,而不是其本身的目标。

尽量保持在每个文件的基础上,重构文件时很容易漏洞百出。尝试将其保持在每个diff的一个重构文件(及其测试)中。

维护第一个差异中的接口首先重构组件的内部结构,然后在后续更改其道具。

编写新测试我们当前的许多测试都是无效的,当您更改组件时,请根据下面的指导原则编写新的测试。

首先关注客户和发货功能,而不是为了干净的代码而关注干净的代码。

所有UI组件都应该位于组件库中。一个很好的经验法则是,如果它从我们的设计系统中导入,那么它就属于团队级别的组件库。

函数组件重于类组件钩子现在被认为是标准的最佳实践,我们应该努力将类组件重构为带有钩子的函数组件。

I18N位于文件的底部,创建一个名为getStrings的函数或一个名为useStrings的钩子来返回组件所需的字符串。不要在组件中内联I18N。

声明性的,而不是命令性的避免像Modal.showInstance这样的函数。跟踪模式的打开/关闭状态。

设计系统样式避免替代设计系统构件中的样式。*在可能的情况下,联系。

.