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),以及他們之間的交互方式:
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 行代碼之間,主要負責三件事:
從 Service 獲得數據(ViewModel)並展示
響應用戶操作,調用相應的 Service 接口
監聽 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.
設計架構也沒有「最好」或者「最正確」的方式,設計本身就是一項極具創造力的工作。但架構是有好壞區別,一個好的架構應該是對團隊成員最為直觀,同時擴展性良好的架構。
開篇先簡單介紹下架構的整體,後續文章會具體分析各個層次的實現細節。