本文是 Uber 的客户端工程师团队是如何开发最新版本司机端的系列文章中的第二篇,代号 Carbon ,是我们拼车业务的一个核心组件。除了其他新功能之外,司机端 APP 还为超过 300万 司机提供收入,引导他们挣钱。2017年我们结合司机的反馈开始对司机端进行重新设计,并在2018年9月份启动了该项目。
重新编写一个APP会引发关于新架构和新设计相关的许多问题。迄今为止,大多数开发者只关注应用程序如何正常工作。虽然以用户为中心的设计理念已经成为软件开发中的一些主流,但是辨别真实有效的用户需求并不容易。
一旦决定 重构Uber司机端, 我们必须以广泛多样的用户群里为基准,寻找如何设计更适用的工作流和保留最适用的产品功能。 我们收集全世界各个城市的Uber司机的反馈,将这些反馈作为我们用户体验的重要初衷。
与此同时,我们也必须考虑到,成百数千的工程师们,他们需要重构APP的同时也要功能迭代。一个经过深思熟虑的APP架构需要帮助工程师,在保证可靠性的同时高效、快速的工作。
幸运的是,Carbon 在满足这些服务时并没有发生冲突,这些服务包括更实用的应用流程,改善司机体验功能,为开发者提供灵活稳定的架构,事实证明,它们结合的非常好。
本篇文章,阐述了我们是为了适应新司机端(代码Carbon)如何提出核心需求,并且讨论使用RIBs架构和插件设计,来满足司机端应用逻辑。
规模发展
自2013年发版以来,Uber司机端积累了需要功能。在未来的四年之后Uber司机端将成为司机的核心工具。随着应用程序的日益复杂,Uber确实做到组织化。上百个功能由横跨公司40多个不同子团队开发和维护。截止2017年1月,Android司机端已经拥有由近200个工程师编写出428,685行代码。iOS司机端由200多工程师贡献720,273行代码。更为重要的是,我们的APP安装在超过300万的设备中,每天超过100个国家中的100万个司机使用。
对于Carbon的成功,我们知道我们需要及时更新所有现有功能(已经一些新功能)的同时,也需要并行更新架构。
一起建设
产品开发需要时刻记住用户。为此,我们想和Uber司机一起创建构建我们的应用。在开发的最初阶段,我们在用户调研上投入大量资金。在11个国家,12个城市中采访了500位司机。
这些访谈对象帮我们设计新司机端的用户体验,确定最重要的功能。但是,除非司机在在真实条件下在实际道路中使用APP,否则无法获取完整的用户体验。我们需要一种方法,它可以允收集用户反馈,快递迭代并每周发布新版本。
图一:在测试期间,为APP使用最初设计。
实现可靠性
最难并且最重要的挑战是,在实际测试中的稳定性。当接触一个新APP,在Alpha和Beta测试阶段可以接受错误和问题。Uber司机端在Beta阶段,真实的司机已经可以在实际道路中可以使用它挣钱了。实现可靠性是Carbon的关键目标。所以,进入首次测试阶段时,我们必须确保Carbon像现有程序一样可靠。
解耦规模化开发
阐述了现在工程的限制后,我们采用了分段性方法,将项目分为四个阶段。每个阶段的目标是开启下个阶段的开发。
第一阶段: 基础设施
我们拥有一个通用模板,来描述我们的APP必须包含哪些因素。这个模板包括网络库,存储库,ReactiveX,分析追踪,崩溃上报和我们的自研的应用架构Ribs。利用这个模板,我们构建了具备初始化架构的程序,包括存储能力,网络,崩溃上报和基础组件。然儿,在这个阶段。程序缺少司机业务功能,这仅仅是我们构建特性的框架。
第二阶段: 应用层设施
使用RIBs框架的一个好处是,它如何将业务逻辑作为应用程序体系结构的核心。在Carbon阶段,一个好的起点是为司机定义高标准的用户状态。这将导致我们为RIBs定义一些基础:
- Root: 当启动应用时,Root启动基于RIBs应用程序的所有必要文件。
- Logged Out: 如果用户没有有效的会话时,我们需要一个RIBs为用户创建一个账户,登录并且获取有效凭证。
- Logged In: 一旦用户身份验证通过,分离注销的RIBs,将登录的RIBs添加到有效的会话中
- Active: 有时一个司机用户可以登录,但是不能做其他操作(他们的账户由于各种原因被封锁)。这个RIBs确保他们有一个有效的会话_并且_允许使用程序。
我们利用RIBs树图,如下图2所示,说明应用架构。这个简单的树形图展示了,如何使用RIBs组件相互关联。
图二:RIBs树图向我们展示了组件关联的可视化方法。
在关注用户阶段,我们可以将UI分离出来。这个方法可以促使从司机中吸取持续反馈建议,并将建议吸纳到设计中。同时促使我们维护用户程序的基本。
第三阶段: 功能框架
在一些基础上,我们重点转移到协作中。在第三阶段,我们的目标是扩大规模,使大约40个团队在同一个APP中可靠无缝的并行工作。基于用户的反馈和设计、产品团队的协作,我们定义出更详细的RIBs组件。
- 任务: 当司机在线和工作时如果体验APP (例如, 导航到乘客位置, 开始行程, 乘客下车, 接另一个单, 等等.).
- 行程 (行程规划): 司机规划行程的关键地区。在此,他们可以清晰的看到他们需要做的:接驾乘客到送餐。如果它是空的,它会给司机从哪里什么时间提供服务的建议。
- 个人中心: 这是司机管理业务的区域。它包含实际行程之外的所有内容:重要通知,评级,收入等等
- 地图: 在司机端中许多和地图相关的功能,例如导航,热力图,和其他使用新RIBs开发的地图库。这是一个在地图框架上的抽象层。有趣的是,如下图展示。这个地图库在APP中视为非核心功能。这意味着,如果地图功能遇到灾难性问题(当然我们希望永远不要发生!),我们可以禁用它,允许司机继续使用。
在合并了框架之后,RIBS树结构变大了,如下面的图三所示。
图三: 按照我们的定义在RIBs树中增加层级和新的功能框架。
在我们细分之前, 我们清晰下在使用的RIBs架构中的一些概念。
Worker
这是一个对象,它直接关联到RIBs生命周期,具有启动/停止生命周期功能方法。换句话说,添加到RIBs的工作者从RIB连接时启动,RIB分离时停止。工作者确保交互(一个RIB的业务逻辑组件单元)不会太大,并且允许更好的分离关注点。(在我们的仓库中可以找到 Android 和 iOS)
插件
插件是一种设计模式,插件允许我们用灵活的方式进行特征标记。(进一步了解Uber如果利用插件 上一篇文章)。首先我们为集成的核心代码定义一个公用的API,然后开发者可以实现自己的API实例。这是因为他们了解代码由遵守这个架构的特征标记隐形保护。把每个插件点想象成微服务体系中一个服务,然后插件工厂作为该服务的消费者,这样将使得插件和API或者他们之间的契约相似。
结合RIBs,插件和工作者,可以定义出架构中的核心和非核心部分。核心组件是必须的组件,并且不能被标记成禁用。另一方面,如果非核心组件引入了重大问题或者导致回归,可以被禁用。在上面的图四种,Map和MyHub是非核心组件,他们可以在不关闭应用程序功能的基础上被禁用。
进一步查看图 4 中 RIBs 树图的细节功能,我们就能看到:如何使用核心 RIBs,Plugins,Workers, 以 worker/plugin 模式去实现 Agenda 的全部功能。Agenda 功能暴露了两个不同的插件点(plugin points): Agenda Worker 和 Agenda Section。 使用 Worker plugin point 集成非用户体验功能,Section plugin point 去扩展用户体验功能。
图四:仔细观察RIBs树的一部分,可以发现集成的多个领域,它允许在灵活添加特性,也允许并行开发。
使用这个设计模式,我们可以理解许多领域APP。例如一些工程师构建登录和注册界面,其他工程师重点开发非核心RIBs的地图架构。
第四阶段: 全员
第四阶段,其他团队加入我们开放了Carbon的开发。由于想要确保每个功能彼此独立,我们创建到了插件框架,因此特性合并是一个相对顺畅的过程。如有必要,一些小RIBs上升为核心组件,但是我们大部分代码仍然包含在插件中,所以我们的架构中一些部分是可选性的(我们计划在后续章节中讨论一些令人兴奋的新特性)。
图五: 如动画所示,RIBs生命周期与他们的各自单元相关
结束语
软件工程的架构是一个通常是在牺牲别的资源条件下去优化团队更为重要的一些指标。虽然我们的方法允许我们优化可靠性、可扩展性和模块化,但是其他方面我们必须妥协。在一个严谨的流程中,我们只允许少量的组件是核心组件。为了维护核心组件的质量,我们拥有一个内部审查团队,他们审查修改核心代码中每个代码。这个流程要求一些工程师投入一些时间去做核心代码审查,这也减慢了其他工程师的提交频率。
大多数应用开发者熟悉MVP或MVC模型。和他们相比,RIBs好像更冗长。RIBs有更多的组件,需要更多的前期规划。较小的软件开发团队也许不需要采用类似的流程。之前我们已经做完一个重构重写我们的乘客端APP,因此我对如何构建Carbon更清晰,我们将我们的学习总结如下:
-
按规模工作: 在Uber, 规模 既是最高限制也是最有价值资源. 300万司机在使用我们的APP,所以我们不能让他们失望。 同时, Uber作为一个技术组件公司,我们已经发展到数百万工程师一起构建应用程序。规模 是我们从规划到推广决策的关键。
-
RIBs to the rescue: Carbon通过使用RIBs和插件,我们在以下方面获得成绩:
- 模块化: 每个功能的开发不依赖于其他功能,除非它是核心RIBs。这个架构使得工程师们并行开发更便捷,不比担心影响其他RIBs。
- 可扩展性: 树形结构可以通过添加子节点纵向扩展,可以通过添加兄弟节点横向扩展,也可以通过不同works将业务分离横向扩展。
- 可靠性: 使用插件完成的核心和非核心概念允许我们禁用非核心代码,所以我们以最小损失代价进行快速转移。
-
共同构建: 如果没有司机的帮助,以及他们在用户调研和测试阶段给我们的建议,我们不可能做到这一点。