Uber 新架構 RIBs 的前世此生

new_rider_app

爲何 Uber 要重構移動端

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 移動平臺都是從一致性出發。平臺共享:架構

  • 核心架構
  • 類名
  • 業務邏輯單元之間的繼承關係
  • 業務邏輯如何劃分
  • 插件點 (名字, 存在,結構等)
  • 響應式編程鏈
  • 統一平臺組件

爲了實現平臺之間的這種通用藍圖,咱們的新移動架構須要清晰的組織和分離業務邏輯,視圖邏輯,數據流和路由。這種架構有助於下降複雜性,簡化可測試性,從而提升工程效率和用戶可靠性。 咱們在其餘架構模式上進行了創新以實現此目標。併發

從 MVC 到 Riblets

考慮到咱們的兩個目標,咱們調查了舊架構能夠改進的地方,並研究了可行的方案。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 (Model-View-Controller)

乘客端應是在大約四年前由少數幾個工程師建立的。 雖然 MVC 模式在當時是有意義的,但隨着程序的規模愈來愈大,也就愈來愈難以管理。 隨着業務的增加和團隊的擴大, MVC 的弊端愈加明顯。具體來講,有兩大問題:

首先,成熟的 MVC 架構常常面臨重量級視圖控制器的困境。例如,RequestViewController 剛開始有 300 行代碼,因爲處理了太多的功能(業務邏輯,數據操做,數據驗證,網絡邏輯,路由邏輯等),如今超過 3,000 行。它變得難以閱讀和維護。

其次,MVC 架構的更新過程是不易維護和測試的。咱們進行了大量實驗,爲用戶推出了新功能。 這些實驗歸結爲 if-else 語句。 每當將 if-else 語句構建在一個具備許多功能函數的類上時,致使幾乎沒法推理,更不用說測試了。 此外,因爲像RequestViewController 和 TripViewController 代碼巨大而且快速增加,所以對應用程序進行更新變得更加空難。 想象一下,進行更改並測試嵌套 if-else 實驗的每種可能組合將是多麼的困難。因爲咱們須要實驗來繼續添加新功能並增長 Uber 的業務,所以這種架構不具有可擴展性。

VIPER

在考慮 MVC 的替代方案時,咱們受到 VIPER 架構的啓發。適用於 iOS 應用程序的簡潔架構。VIPER 爲 MVC 提供了一些關鍵優化。首先,它提供了更多的抽象。Presenter 橋接視圖邏輯和業務邏輯。Interactor 處理純粹的數據操做和數據驗證,包括向服務層發起調用,例如登陸或者發單。最後,Router 啓動跳轉,例如將用戶從首頁帶到確認頁。其次,使用 VIPER 方法,Presenter 和 Interactor 是普通對象,所以咱們能夠進行簡單的單元測試。

但咱們也發現了 VIPER 的一些缺點。它是 iOS 獨有架構,意味着咱們必須爲 Android 作出權衡。因爲整個應用程序被固定在視圖樹上,也就意味着狀態由視圖驅動。 Interactor 必須經過 Presenter 操做應用程序的業務邏輯,所以須要暴露業務邏輯給 Presenter。至此,經過緊密耦合的視圖樹和業務樹,很難實現僅包含業務邏輯或僅包含視圖邏輯的業務節點,沒法達到解藕的目的。

雖然 VIPER 對使用的 MVC 模式進行了重大改進,但它並無徹底知足,清晰的模塊化定義,和高可擴展性。因此咱們在兼顧 VIPER 優點,同時規避其架構模式缺點的基礎上,實現了咱們本身的架構: Riblets。

Riblets: Uber 乘客端架構

在咱們的新架構模式中,業務邏輯被分解爲小的,可獨立測試的單元,每一個單元目的明確,遵循單一責任原則。 咱們使用 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

Builder 實例化全部主要 Riblet 單元並定義依賴關係。 在車型切換 Riblet 中,此單元定義城市流(特定城市的數據流)依賴關係。

Component

Component 獲取並實例化 Riblet 的依賴項。 這包括服務,數據流以及其餘不是主要 Riblet 單元的內容。 車型切換組件獲取並實例化城市流依賴關係,將其與對應的網絡事件進行關聯,並將其注入到 Interactor。

Routers

Routers 經過添加和刪除 子Riblets 造成應用程序樹,同時驅動組件內 Interactor 的生命週期。 這些決定由外部 Interactor 傳遞。路由器包含兩個業務邏輯:

  1. 添加和刪除組件
  2. 子組件間狀態切換

車型切換 Riblet 沒有任何子 Riblets。 其父 Riblet 的 Router, 確認 Riblet 負責添加車型切換的 Router 並將其 Views 添加到 View 層次結構中。 而後,一旦選擇了車型,車型切換 Router 將停用其 Interactor。

Interactors

Interactors 執行業務邏輯:

  • 調用服務來啓動操做,好比請求搭車
  • 調用服務來獲取數據
  • 決定要轉換到下一個的狀態。 例如,若是根 Interactor 監聽到用戶的身份驗證令牌過時,它會向其 Router 發送切換到 「歡迎」 狀態的請求。

車型切換 Interactor 包含城市流數據,包括該城市服務的車型,訂價信息,預估行程時間和車輛視圖。 它將此信息傳遞給 Presenter。 若是用戶從拼車切換到優享,則 Interactor 會從 Presenter 接收此信息。 而後它會收集相關數據傳給 View,這樣它就能夠顯示 uberX 車輛和預估行程時間。 簡而言之,Interactor 執行隨後 View 中顯示的全部業務邏輯。

View (Controller)

視圖構建和更新UI,包括實例化和佈局 UI 組件,處理用戶交互,UI 組件數據填充和動畫。 車型切換 Riblet 的 View 顯示它從 Presenter 接收的數據(車型選項,訂價,ETA,地圖上的車輛視圖)並反饋用戶操做(即車型切換)。

Presenter

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 構建應用程序

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。

Riblets 間通訊

當 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 測試以及編寫新功能。最重要的是,咱們但願進行全球推廣,以便更快地將新應用程序交付給用戶,所以咱們從設計,功能,本地化,設備和測試角度考慮了全球變化。 雖然已經投放市場,但咱們新架構下的工做纔剛剛開始。

相關文章
相關標籤/搜索