Uber 基于一个简单的概念:一键出行。 从最初优享到如今提供的一系列产品,天天在数百个城市协调数百万次乘车。 为了应对和支持2017年及之后的发展,咱们迫切的须要从新设计咱们的移动端架构。html
但从哪里开始? 咱们决定从新开始。因而咱们决定彻底重构并从新设计咱们的乘客端。 因为不用被以前的设计和代码限制,在重构上咱们有很大的发挥空间。结果就是你今天看到的这个时尚的新应用, 它在iOS和Android上实现了新的移动架构。接下来的文章将介绍咱们的新移动端架构 Riblets,让你了解为何咱们须要建立这种新架构模式,以及它如何帮助咱们达成目标。ios
虽然共享出行仍然是 Uber 背后的驱动理念,但咱们的产品已发展成为功能复杂的APP,咱们原有的移动架构没法与之匹配。 随着乘客端App的新功能扩展,工程挑战和技术债务不断累积。增长了诸如拼车 ,预定乘车 和促销车辆视图等功能,致使工程的复杂性逐步升高。 咱们的行程模块变得愈来愈大,难以测试。 加入小变化有可能影响到应用程序的其余部分,使得功能尝试增长额外调试任务,从而抑制了咱们快速迭代和功能实验。 为了给全部 Uber 用户的高质量体验,咱们须要一种方法,从新找回起点的简单,同时考虑今天的处境和将来的目标。git
对于乘客和 Uber 工程师来讲,新的应用程序必须简单。 为了适用于不一样的群体,咱们的两个主要目标是:持续增长有效的核心用户体验,而且容许在系列产品需求序列中作大胆实验。github
从工程方面来讲,咱们正在努力使 Uber 的行程主流程的可靠性达到 99.99%。 实现99.99%的可靠性意味着咱们每一年只能有一个累计小时的停机时间,一周的停机时间为一分钟,每10,000次运行只有一次失败。编程
为了实现这一目标,新架构定义并实现了核心和可选代码的框架。 核心代码包括注册,获取,完成或取消行程所需的一切代码。 对核心代码的更改和添加须要通过严格的审核流程。 可选代码能够下降审查力度,能够在不中止核心业务的状况下关闭。 这种代码隔离机制使咱们可以尝试新功能,并在异常状况下自动关闭它们,而不会干扰乘车体验。后端
咱们须要一个平台,一百个不一样的项目团队和数千名工程师能够快速构建高质量的功能,并在乘客端上进行创新,而不会影响核心用户体验。 所以,咱们提供了新的移动端架构,具备跨平台兼容性,确保iOS和Android工程师均可以在统一的基础上工做。服务器
从历史上看,在 iOS 和 Android 上发布最好的应用程序涉及不一样的架构、库设计和分析方法。 可是,新架构致力于在两个平台上使用相同的最佳模式和实践。 这给了咱们学习两个平台的机会。 因为一个平台的经验教训能够预先解决另外一个平台上的问题,从而避免了一样的错误在两个平台重复出现。 所以,iOS 和 Android 工程师能够更轻松地进行协做,而且能够并行处理新功能。网络
虽然在某些状况下,平台之间能够也应该是不一样的(例如 UI 实现),可是 iOS 和 Android 移动平台都是从一致性出发。平台共享:架构
为了实现平台之间的这种通用蓝图,咱们的新移动架构须要清晰的组织和分离业务逻辑,视图逻辑,数据流和路由。这种架构有助于下降复杂性,简化可测试性,从而提升工程效率和用户可靠性。 咱们在其余架构模式上进行了创新以实现此目标。并发
考虑到咱们的两个目标,咱们调查了旧架构能够改进的地方,并研究了可行的方案。Uber 旧的代码遵循[MVC 模式](https://developer.apple.com/library/content/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html)。咱们调查了其余模式,特别是[VIPER](https://mutualmobile.com/posts/meet-viper-fast-agile-non-lethal-ios-architecture-framework),咱们最终用它来建立 Riblets。Riblets 的核心创新是业务逻辑驱动,而不是视图逻辑驱动。 若是您不熟悉 MVC 和 VIPER,请阅读一些[关于现代 iOS 架构模式的文章](https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52#.ba5863nnx),而后回过头来看看在 Uber 采用它们的利弊。
乘客端应是在大约四年前由少数几个工程师建立的。 虽然 MVC 模式在当时是有意义的,但随着程序的规模愈来愈大,也就愈来愈难以管理。 随着业务的增加和团队的扩大, MVC 的弊端愈加明显。具体来讲,有两大问题:
首先,成熟的 MVC 架构常常面临重量级视图控制器的困境。例如,RequestViewController 刚开始有 300 行代码,因为处理了太多的功能(业务逻辑,数据操做,数据验证,网络逻辑,路由逻辑等),如今超过 3,000 行。它变得难以阅读和维护。
其次,MVC 架构的更新过程是不易维护和测试的。咱们进行了大量实验,为用户推出了新功能。 这些实验归结为 if-else 语句。 每当将 if-else 语句构建在一个具备许多功能函数的类上时,致使几乎没法推理,更不用说测试了。 此外,因为像RequestViewController 和 TripViewController 代码巨大而且快速增加,所以对应用程序进行更新变得更加空难。 想象一下,进行更改并测试嵌套 if-else 实验的每种可能组合将是多么的困难。因为咱们须要实验来继续添加新功能并增长 Uber 的业务,所以这种架构不具有可扩展性。
在考虑 MVC 的替代方案时,咱们受到 VIPER 架构的启发。适用于 iOS 应用程序的简洁架构。VIPER 为 MVC 提供了一些关键优化。首先,它提供了更多的抽象。Presenter 桥接视图逻辑和业务逻辑。Interactor 处理纯粹的数据操做和数据验证,包括向服务层发起调用,例如登陆或者发单。最后,Router 启动跳转,例如将用户从首页带到确认页。其次,使用 VIPER 方法,Presenter 和 Interactor 是普通对象,所以咱们能够进行简单的单元测试。
但咱们也发现了 VIPER 的一些缺点。它是 iOS 独有架构,意味着咱们必须为 Android 作出权衡。因为整个应用程序被固定在视图树上,也就意味着状态由视图驱动。 Interactor 必须经过 Presenter 操做应用程序的业务逻辑,所以须要暴露业务逻辑给 Presenter。至此,经过紧密耦合的视图树和业务树,很难实现仅包含业务逻辑或仅包含视图逻辑的业务节点,没法达到解藕的目的。
虽然 VIPER 对使用的 MVC 模式进行了重大改进,但它并无彻底知足,清晰的模块化定义,和高可扩展性。因此咱们在兼顾 VIPER 优点,同时规避其架构模式缺点的基础上,实现了咱们本身的架构: Riblets。
在咱们的新架构模式中,业务逻辑被分解为小的,可独立测试的单元,每一个单元目的明确,遵循单一责任原则。 咱们使用 Riblets 做为这些模块化部件,整个应用程序结构为 Riblets 树。
经过 Riblets,咱们将职责分配给六个不一样的组件,进一步抽象业务和视图逻辑:
Riblets 与 VIPER 和 MVC 的区别是什么?路由由业务逻辑而非视图逻辑引导。这意味着应用程序由信息流和决策流驱动,而不是 Presenter。在Uber,并不是每一个业务逻辑都与用户看到的视图相关。不是将业务逻辑集中到 MVC 中的 ViewController 或经过 VIPER 中的 Presenter 操做应用程序状态。咱们能够为每一个业务逻辑提供不一样的 Riblets,这些 Ribltes 能够组合出不一样意义的逻辑分组。 Riblet 模式被设计为跨平台的,达到统一 Android 和 iOS 架构的目的。
每一个 Riblet 由 Router,Interactor 和 Builder 及其 Component 和可选的 Presenters 和 Views 组成。Router 和 Interactor 处理业务逻辑,而 Presenter 和 View 处理视图逻辑。
让咱们使用车型切换 Riblet 做为示例,肯定每一个 Riblet 单元负责的内容。
新乘客端APP,车型切换功能。
Builder 实例化全部主要 Riblet 单元并定义依赖关系。 在车型切换 Riblet 中,此单元定义城市流(特定城市的数据流)依赖关系。
Component 获取并实例化 Riblet 的依赖项。 这包括服务,数据流以及其余不是主要 Riblet 单元的内容。 车型切换组件获取并实例化城市流依赖关系,将其与对应的网络事件进行关联,并将其注入到 Interactor。
Routers 经过添加和删除 子Riblets 造成应用程序树,同时驱动组件内 Interactor 的生命周期。 这些决定由外部 Interactor 传递。路由器包含两个业务逻辑:
车型切换 Riblet 没有任何子 Riblets。 其父 Riblet 的 Router, 确认 Riblet 负责添加车型切换的 Router 并将其 Views 添加到 View 层次结构中。 而后,一旦选择了车型,车型切换 Router 将停用其 Interactor。
Interactors 执行业务逻辑:
车型切换 Interactor 包含城市流数据,包括该城市服务的车型,订价信息,预估行程时间和车辆视图。 它将此信息传递给 Presenter。 若是用户从拼车切换到优享,则 Interactor 会从 Presenter 接收此信息。 而后它会收集相关数据传给 View,这样它就能够显示 uberX 车辆和预估行程时间。 简而言之,Interactor 执行随后 View 中显示的全部业务逻辑。
视图构建和更新UI,包括实例化和布局 UI 组件,处理用户交互,UI 组件数据填充和动画。 车型切换 Riblet 的 View 显示它从 Presenter 接收的数据(车型选项,订价,ETA,地图上的车辆视图)并反馈用户操做(即车型切换)。
Presenters 管理 Interactors 和 Views 之间的通讯。 从 Interactors 到 Views,Presenter 将业务模型转换为 View 能够显示的模型。 对于车型切换,这包括订价数据和车辆视图。 从 Views 到 Interactors,Presenters 将用户交互事件(例如,点击按钮选择车型)转换为 Interactors 中的相应操做。
Riblets 只有一个 Router 和 Interactor,但能够有多个 View 部分。仅处理业务逻辑且没有用户界面元素的 Riblet 没有视图部分。 所以,Riblets 能够是单视图(一个 Presenter 和一个 View),多视图(一个 Presenter 和多个 Views,或多个 Presenter 和 Views),或者是无视图(没有 Presenter 和 View)。 这容许业务逻辑树的结构和深度与视图树不一样,视图树将具备更平坦的层次结构。 这有助于简化页面切换。
例如,乘车 Riblet 是一个无视图的 Riblet,用于检查用户是否有有效的行程。若是已经开始行程,它添加行程 Riblet,将行程显示在地图上。若是没有,它将添加请求 Riblet,请求 Riblet 将在屏幕显示,容许用户请求行程。像乘车 Riblet 这样没有视图逻辑的 Riblet,经过分解业务逻辑驱动应用程序,在支持这种新体系结构的模块化方面,发挥了重要做用。
Riblets 组成了应用程序树,而且常常须要进行通讯以便更新信息或将用户带到下一阶段。 在咱们讨论他们如何通讯以前,让咱们首先了解数据在一个 Riblet 中是如何流动的。
Interactors 拥有状态的做用范围和业务逻辑。该单元进行服务调用获取数据。 在新架构中,数据是单方向流动的。 它从 Service 到 Model Stream,而后从 Model Stream 到 Interactor。 来自服务器的交互,调度和推送通知能够要求 Service 对 Model Stream 进行更改。Model Stream 生成不可变模型。 这强制要求 Interactors 类必须使用服务层来更改应用程序的状态。
示例流程:
从后端服务到视图: 服务调用(如状态)从后端获取数据。 将数据放置在不可变 Model Stream 上。 Interactor 监听新数通知并将其传递给 Presenter。 Presenter 格式化数据并将其发送给 View。
从视图到后端: 用户点击按钮(如登陆),而后 View 将交互传递给 Presenter。 Presenter 在 Interactor 上调用登陆方法,该方法调用 Service 进行登陆。 返回的令牌由 Service 在数据流上发布。 Interactor 监听数据流,收到通知后 Interactor 切换 Riblet 到首页 Riblet。
当 Interactor 作出业务逻辑决策时,它可能须要通知另外一个 Riblet(例如,完成)并发送数据。为实现此目的,作出业务逻辑决策的 Interactor 调用另外一个 Riblet 的 Interactor 。
一般,若是通讯是 Riblet 树上,从子 Riblet 传递到父 Riblet 的 Interactor,则该接口被定义为侦听器。侦听器几乎老是由父 Riblet 的 Interactor 实现。若是通讯向下传递给子 Riblet,则应将接口定义为代理,并由子 Riblet 的 Interactor 实现。代理仅用于 Riblet 单元之间的同步通讯,例如父 Interactor 与子 Interactor 之间的同步。
特别是对于向下通讯,做为代理的替代方法, 父 Riblet 能够选择将可观察的数据流暴露给子 Riblet 的 Interactor。而后,父 Riblet 的 Interactor 能够经过此流将数据发送到子 Riblet 的 Interactor。在大多数用于发送数据的向下通讯中,这应该是首选的通讯方法。
例如,车型切换 Interactor 肯定已选择车型时,它会调用其侦听器以传递所选的车辆视图 ID。侦听器由确认 Interactor实现。而后,确认 Interactor 存储车辆视图 ID,以即可以在服务请求中发送,调用其 Router 分离车型切换 Riblet。
经过以上方式构建 Riblets 内部和 Riblets 之间的数据流通讯,咱们可以确保在正确的页面正确的时间出现正确的数据。由于 Riblets 基于业务逻辑造成应用程序树,因此咱们能够经过业务逻辑(而不是视图逻辑)来路由通讯。这对咱们的业务意义重大,并最终有助于代码隔离,防止应用程序开发变得过于复杂。
当咱们从新开始乘客端时,但愿提升乘客体验的可靠性和为将来的应用程序开发创建标准规范。建立新架构对于实现这两个目标相当重要。
Riblets 有明确的职责划分,所以测试更加简单。每一个 Riblet 都是可独立测试的。经过更充分的测试,当推出更新时,咱们能够对应用的可靠性更有信心。因为每一个 Riblet 只负责一个任务,所以很容易将 Riblet 及其依赖项分离到核心代码和可选代码中。经过对核心代码进行更严格的审查,咱们能够对核心流程的可用性更有信心。
咱们提供了核心流程全局回滚到可用状态的能力。全部可选代码都具有开关能力,若是部分功能有问题,能够将其关闭。在最糟糕的状况下,咱们能够关闭所有可选代码,保留默认的核心流程。因为咱们在核心代码上有超高的标准,能够确保咱们的核心流程始终有效。
Riblets 帮助咱们尽量缩小和分离功能。清晰的分离业务和视图逻辑,将有助于防止咱们的代码库变得过于复杂并使其易于工做。因为新架构与平台无关,所以 iOS 和 Android 工程师能够轻松了解对方如何开发,从一方的错误中吸收教训,并共同推进 Uber 向前发展。因为 Riblets 帮助咱们将可选代码与核心代码分开,所以实验将不太容易对核心体验产生附带影响。咱们将可以在 Riblet 架构中将新功能做为插件进行尝试,而没必要担忧它们可能会意外地将 uberX 和 uberPOOL 体验置于bug 的风险之中。
因为 Riblets 增强了抽象和责任分离,而且有明确的数据流和通讯路径,所以持续开发变得很容易。这种架构将在将来几年内为咱们服务。
咱们的新架构使咱们为将来的发展作好了准备。最新的重构意味着彻底重作乘客端的代码,从新实现之前存在的内容,执行用户研究,案例研究,A/B 测试以及编写新功能。最重要的是,咱们但愿进行全球推广,以便更快地将新应用程序交付给用户,所以咱们从设计,功能,本地化,设备和测试角度考虑了全球变化。 虽然已经投放市场,但咱们新架构下的工做才刚刚开始。