為什麼 Uber 要重構移動端
Uber 基於一個簡單的概念:一鍵出行。 從最初優享到現在提供的一系列產品,每天在數百個城市協調數百萬次乘車。 為了應對和支援2017年及以後的發展,我們迫切的需要重新設計我們的移動端架構。
但從哪裡開始? 我們決定重新開始。於是我們決定完全重構並重新設計我們的乘客端。 由於不用被之前的設計和程式碼限制,在重構上我們有很大的發揮空間。結果就是你今天看到的這個時尚的新應用, 它在iOS和Android上實現了新的移動架構。接下來的文章將介紹我們的新移動端架構 Riblets,讓你瞭解為什麼我們需要建立這種新架構模式,以及它如何幫助我們達成目標。
目標
雖然共享出行仍然是 Uber 背後的驅動理念,但我們的產品已發展成為功能複雜的APP,我們原有的移動架構無法與之匹配。 隨著乘客端App的新功能擴充套件,工程挑戰和技術債務不斷累積。增加了諸如拼車 ,預約乘車 和促銷車輛檢視等功能,導致工程的複雜性逐步升高。 我們的行程模組變得越來越大,難以測試。 加入小變化有可能影響到應用程式的其他部分,使得功能嘗試增加額外除錯任務,從而抑制了我們快速迭代和功能實驗。 為了給所有 Uber 使用者的高質量體驗,我們需要一種方法,重新找回起點的簡單,同時考慮今天的處境和未來的目標。
對於乘客和 Uber 工程師來說,新的應用程式必須簡單。 為了適用於不同的群體,我們的兩個主要目標是:持續增加有效的核心使用者體驗,並且允許在系列產品需求序列中做大膽實驗。
可靠性
從工程方面來說,我們正在努力使 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 傳遞。路由器包含兩個業務邏輯:
- 新增和刪除元件
- 子元件間狀態切換
車型切換 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 測試以及編寫新功能。最重要的是,我們希望進行全球推廣,以便更快地將新應用程式交付給使用者,因此我們從設計,功能,本地化,裝置和測試角度考慮了全球變化。 雖然已經投放市場,但我們新架構下的工作才剛剛開始。