Intro

前不久我們上線了一款新的 App - Glow Baby,App 針對 0 - 12 個月大的新生寶寶,提供爸爸媽媽全面、健康、科學的育兒知識,幫助記錄寶寶成長的點點滴滴。在 Glow Baby 的開發中,我們也做了一些新的嘗試 - 使用 Swift 開發,並基於 Swift 的語言特點設計了新的 iOS App 架構。

除了 Community 這部分的程式碼是作為一個私有的 Repo 引入,Glow Baby 基本是 100% Swift 程式碼。Glow Baby iOS 團隊都是第一次接觸 Swift,過程中我們踩過很多坑,遇到過很多抓狂的問題。但總體上,寫 Swift 更加有趣,所有的努力最終也證明是值得的:App 執行更加流暢,程式碼更整潔可讀性更高,我們開發效率也大大提高。

Baby App 跟 Glow 的其他幾個 App 都是較為複雜的 App,因為像日記記錄、本地儲存、網路請求,伺服器端資料的同步這些技術難點都有涉及。這些問題也都要求多執行緒程式設計。特別是資料同步,如何增量記錄資料的增刪改,什麼時機跟伺服器端進行同步。解決這些技術難題是非常有意思的工作,也是架構設計的創造性和樂趣所在。

接下來的一系列文章我們來看看 Baby App iOS 的應用架構。有些設計是基於 Swift 的語言特點的考慮,但並不妨礙整體的架構思路被應用在 Objective-C,甚至 Android 的 App 上。

MV(X)

在介紹 Glow Baby 的應用架構之前,先來看看目前 iOS 上最基礎的架構 MVC,以及為解決 MVC 的毛病而誕生的其他幾個架構,如 MVVM、VIPER 等。

Cocoa 的很多技術跟架構都是基於 MVC。而且無論是文件、示例程式碼,還是建立一個專案時提供的模板程式碼,Apple 都鼓勵開發者去使用 MVC。MVC 定義了 App 裡物件的角色(Model-View-Controller),以及他們之間的互動方式:

MVC

  • Model: 表示業務資料物件
  • View:展現資料的 UI
  • Controller:Model 跟 View 之間的粘合劑。一方面對 View 上的行為作出反應,通常會涉及到 Model 的更改;另一方面將 Model 的改動反映到 View 上

由於 Controller 作為粘合劑的存在,View 和 Model 只需要跟 Controller 互動,而不知道另一方的存在。這樣,View 和 Model 作為獨立可複用的元件,Controller 裡處理業務邏輯。聽起來這樣的架構很清晰直觀,實際應用中,MVC 對於不是很複雜的 App 也是非常高效的。但對稍複雜些的 App,MVC 使用起來就會非常吃力。

你可能聽過 MVC 也被簡稱為 Massive View Controller,這就是原因所在 - View Controller 承擔的職責太多:

  • 網路請求
  • 資料訪問和儲存
  • UI 的調整和組合
  • 業務邏輯
  • View 的 delegate、data source
  • 狀態的維護

與單一責任準則(Single Responsibility Principle)背道而馳。過於臃腫的 View Controller 使 App 的維護成本非常高。我們的第一個 App - Glow 其實就是這個樣子,儘管我們已經把網路請求以及資料訪問和儲存放到了 Model 裡,但由於物件邊界的定義不夠清晰,大部分 View Controller 依然很臃腫,上千行的 View Controller 很常見。關於 View Controller 有個準則:如果一個 View Controller 超過了 300 行程式碼,那它一定做了責任範圍以外的事。更不幸的是由於一些職責移交給 Model,導致 Model 也變得臃腫起來。原來唯一可以做 Unit Test 的 Model 現在測試也很困難。

為解決 Massive View Controller 的問題,MVVM、VIPER 等架構應運而生。這裡不再詳細介紹這些架構,有興趣的讀者可以自行去 Google。

Baby App 沒有使用 MVVM 和 VIPER。因為:

  • 不夠直觀,提高了整體程式碼的複雜度,對於新入職的員工有一定學習成本
  • 要發揮 MVVM 的優勢,需要有 Reactive。Reactive 增加學習成本的同時,也讓除錯變得更困難。
  • VIPER 雖然能平衡責任的分配,但由於引入過多物件,維護成本高。一個簡單的頁面也要求新增多個類和大量傻瓜程式碼

所以我們結合自己的需求和 Swift 的語言特點設計了面向服務的架構(Service Oriented Architecture)。

Service Oriented Architecture

面向服務的架構在伺服器端的開發中很常見,它把業務分成了多個邏輯獨立的元件。一個元件相當於一個 Service,封裝了與其業務相關的功能,如 UserService 負責使用者的註冊、登入等,而 BabyService 有 Baby 的增加、移除、以及資料的記錄等。Glow 伺服器端的架構實際就是面向服務的。在 Baby App iOS 架構中引入 Service 的概念,是 App 開發過程中迭代的結果,靈感也是來自我們伺服器端的架構。

可以看到,Service 是對整個架構縱向邏輯切分的結果。拋開業務邏輯談 Service 意義不大,Service 通常與資料庫表的設計緊密相關。

橫向的邏輯切分將 Baby App iOS 的架構自上而下切分成三個層(Layer):

  • 應用層(Application Layer)
  • 服務層(Service Layer)
  • 資料層(Data Access Layer)

Architecture Overview

服務層和資料層把複雜的邏輯封裝起來,作為 Framework 提供介面給上層呼叫。應用層只能呼叫服務層暴露出來的介面,而不能直接呼叫資料層。層次結構加強了可重用性和可測試性。應用層呼叫服務層提供的簡單介面獲得資料或者實行使用者操作。服務層也不需要知道資料層中網路請求,伺服器同步,以及資料持久化的具體實現。服務層,資料層,以及應用層都能很容易實現各自的單元測試(Unit Test)。

Framework 是很棒的工具。把服務層和資料層打包成 Framework,不僅幫助構建解耦可重用的程式碼,同時 App 的結構和業務邏輯也更加清晰。

應用層(Application Layer)

應用層也可以叫展示層(Presentation Layer),負責 UI展示邏輯。從 Code 角度說,就是 UIView 跟 UIViewController 的集合。複雜的邏輯都封裝到了下層,UIViewController 就變得十分輕量。在 Glow Baby 中,一個 View Controller 通常 200 至 300 行程式碼之間,主要負責三件事:

  1. 從 Service 獲得資料(ViewModel)並展示
  2. 響應使用者操作,呼叫相應的 Service 介面
  3. 監聽 Service 層發出的訊息,並執行相應操作,如更新 UI

從 Service 獲取的 ViewModel 例項並不是 NSManagedObject 或者其他持久化的 model 例項,跟 MVVM 中的 ViewModel 也不一樣。在 Baby App 中,它只是簡單的 Swift Struct,提供應用層需要的資料值。使用 Struct 的好處主要是:

  • 值型別(Value Type): 簡單、容易理解,執行緒安全
  • 鬆耦合的 View Controller,減少 View Controller 之間可能的互動
  • 減少了 Statefulness 和 Mutability
  • 更高效、佔用更少記憶體

使用 Struct 也就意味著想要底層持久化 Model 的更改放映到 UI 上,你必須通過 Service 再抓一次資料。也許有人認為這是使用 Struct 的一個缺點。其實不是,這應該是優點。因為 Immutable 的 ViewModel ,讓 View Controller 變得更加簡單,你不用擔心其他地方的程式碼會更改你的 ViewModel 例項。除錯起來也會更加方便,程式碼更容易理解、可讀更高。WWDC 中有好幾個視訊都對 Struct 的使用和優勢進行了詳解。

Baby App 支援 Theme,因此 Baby App 的 View Controller 還會呼叫 Theme 物件來設定 UI 的樣式。但對 View 樣式的設定都封裝在了 Theme 裡,所以並沒有增加太多程式碼量及 View Controller 的複雜度。

服務層(Service Layer)

服務層定義了一系列 Service 和供給應用層使用的 ViewModel。Service 封裝了 App 主要的業務邏輯,負責把底層持久化的 Model 和網路請求返回的 JSON 轉換為 ViewModel,再提供給應用層使用。這樣的分離即加強了 Immutablility 和 Statelessness,也讓應用層中的 ViewController 更輕量,只需幾行 Service calls。Service 雖然承擔大部分業務邏輯,但一個 Service 通常也就 300 行左右的程式碼量,這得益於資料層的封裝和抽象。

資料層(Data Access Layer)

資料層的作用是提供簡化的資料訪問介面,主要有 3 個模組:

  • 資料儲存(Persistence)
  • 網路請求(Network)
  • 資料同步(Data Synchronization)

資料儲存我們使用的是 Core Data,也可以用 Realm 或者其他資料庫代替。網路請求我們使用了 Moya 進行抽象,使 API 的設計和呼叫更簡潔,並支援我們 Server 自定義的錯誤。資料同步模組,會自動同步本地和伺服器端的使用者資料。

Conclusion

在 iOS 上,MVC 因 Controller 的臃腫而遭到眾人詬病。但其實 MVC 作為最基礎的設計模式,展現了一個架構的精髓 - 抽象分離。這是我們應該學習思考的,而不是盲目從其他架構模式中選擇一個來代替 MVC。Glow Baby iOS 的架構從可以看作是一種 MVC。從整體看,資料層是 Model,服務層是 Controller,應用層是 View。而如果看細節的地方,應用層跟服務層提供的 ViewModel 也可以看做一個 MVC:ViewModel - UIViewController - UIView.

設計架構也沒有「最好」或者「最正確」的方式,設計本身就是一項極具創造力的工作。但架構是有好壞區別,一個好的架構應該是對團隊成員最為直觀,同時擴充套件性良好的架構。

開篇先簡單介紹下架構的整體,後續文章會具體分析各個層次的實現細節。