MVC 在 iOS 中的最佳實踐

本文翻譯自《Model-View-Controller (MVC) in iOS: A Modern Approach》


每一位剛?cè)腴T的 iOS 開發(fā)者都會接觸到大量的知識信息,這些信息對我們學習一門新語言、新框架包括蘋果推薦的 MVC 設計模式來說非常重要。

要跟上 iOS 發(fā)展的腳步是一件不容易的事,很多時候開發(fā)者并沒有對 MVC 引起足夠的重視,然而很多問題確是由此導致的。

這篇文章會幫助你繞開 MVC 實踐中常見的陷阱。你可以學習到一種更現(xiàn)代的方法來正確地使用 MVC 開發(fā)你的 App。

而在文章的結(jié)尾,你將會了解如何防止架構(gòu)上錯誤給你的開發(fā)工作埋下隱患。讓我們開始吧!

什么是 MVC

提示:如果你已經(jīng)了解 MVC 的概念,你可以放心地跳過下面內(nèi)容的開頭部分,直接從 MVC 的實踐開始。

MVC,顧名思義是由 Model 層、View 層和 Controller 層組成:

  • Model:數(shù)據(jù)存放的地方。比如數(shù)據(jù)的持久化、數(shù)據(jù)模型對象、數(shù)據(jù)的解析以及網(wǎng)絡請求的代碼都會放在這里。
  • View:用戶直接交互的地方。這里的類基本都是可以復用的,這些類里沒有特殊的邏輯。比如,UILabel 就是把文本展示到屏幕上,并且它很容易被重用。
  • Controller:Model 和 View 的中介,比較典型的是我們會在這里使用代理模式。在理想的情況下 View 對 Controller 來說是透明的。Controller 會通過一個抽象比如協(xié)議來和 View 進行交流,就像 UITableView 通過 UITableViewDataSource 來和它的數(shù)據(jù)源進行交流一樣。

當你把這些放在一起時候,它應該是這樣的:



是不是很簡單呢?

但是常言道:細節(jié)決定成敗。只有當你真正實踐 MVC 的時候才會發(fā)現(xiàn)事情并不是想象中那么容易。

蘋果官方的 MVC 文檔 對 MVC 有詳細的闡述,這會讓你對 MVC 有一個系統(tǒng)的理論了解,幫助你避免潛在的問題。

但是,僅僅有理論是遠遠不夠,實踐才是檢驗真理的唯一標準。

MVC 的最佳實踐

雖然 MVC 的理論比較容易理解,但是在實踐的過程中我們還是會遇到很多棘手的問題。讓我們來著手解決這些問題吧。

View 層

當用戶使用你的 App 時,他們大部分時間就是在和 View 層打交道。View 層應該是 App 中最直白的部分,因為它不包含任何業(yè)務邏輯。在代碼層面,你通??梢栽谶@一層看到:

  • UIView 的子類們。從最基本的 UIView 到復雜的 UI 控件。
  • 一個 UIViewController(可以論證的)。我個人認為它應該屬于這一層,因為 UIViewController 和其根 UIView 以及它的生命周期(loadView, viewDidLoad) 是密不可分的。當然不是所有人都同意。
  • UIViewController 的 animations 和 transitions。
  • UIKit/AppKitCore AnimationCore Graphics 中的部分類。

這一層的代碼異味(Code smell)可能有多種表現(xiàn)形式,但是總的來說就是在 View 層做了和 UI 不相關的事。一個典型的代碼異味就是在 UIViewController 中做網(wǎng)絡請求。

為了趕 deadline,往 UIViewController 中扔一堆代碼是一件很誘惑人的事。最好別這樣做,也許這在當下能給你節(jié)省幾分鐘時間,但是以后,你可能會為了找一個 bug 而花費幾個小時,或者當你想在另一個 view controller 中重用這段代碼的時候發(fā)現(xiàn)這很困難。

把你的 View 層和下面的清單進行核對:

  • 它是否和 Model 層進行交互?
  • 它是否包含任何業(yè)務邏輯?
  • 它是否做了一些和 UI 不相關的事?

如果滿足上述任何一個條件,那么是時候?qū)δ愕?View 層進行清理和重構(gòu)了。

當然,這些規(guī)則不是鐵的定律,有時候由于各種原因你不等不違背。盡管如此,對它們抱以尊重還是很有必要的。

最后,如果你把這些類寫得很好,你總是可以重用它們。如果你不相信我,就看看 GitHub 上 UI 組件的數(shù)量吧。

Controller 層

Controller 層是你的 App 中最少重用的部分,因為這里面包含很多特定的邏輯。這并不奇怪,有些東西在你的 App 里有用,但是對其它 App 來說沒有任何用處。

通常,在這一層中你會思考這些問題:

  • 先訪問持久化數(shù)據(jù)還是網(wǎng)絡數(shù)據(jù)?
  • 多久刷新一次 App?
  • 頁面的在不同的條件下應該如何跳轉(zhuǎn)?
  • 當 App 進入后臺的時候,哪些需要被清理?

你應該把 Controller 層當作 App 的大腦:它決定了下一步會發(fā)生什么。你會經(jīng)常測試這些類,以確保一切都是如期運行。

舉個例子

現(xiàn)在你應該對 Controller 層有了更好的認識,讓我們來看一個簡單例子。

提示:如果你想了解這在 App 環(huán)境下是如何工作的,可以下載我為你準備的簡單 App。

想象一下你有一個 UIViewController 的子類,它想知道參加今年 WWDC 的人員名單。為了達到這個目的,它會利用一個 controller 類。因為蘋果推薦我們應該重視從一個協(xié)議開始,所以我們會這么做:

enum UIState {
    case Loading
    case Success([Attendee])
    case Failure(Error)
}
 
protocol WWDCAttendesDelegate: class {
    var state: UIState { get set}
}

我們先將 state初始化為 Loading , 然后當參加 WWDC 的人員名單加載成功(或者失敗)的時候更新 state 值。

因為我們不希望在 UIViewController 中處理返回數(shù)據(jù),所以用一個單獨的對象(WWDCAttendeesUIController)來實現(xiàn)WWDCAttendesDelegate。這樣分離的操作可以讓我們對 WWDCAttendeesUIController 進行獨立的測試。

下一步就是為 Controller 創(chuàng)建一個抽象,你可以把它注入到 UIViewController 中:

protocol WWDCAttendeesHandler: class {
 
    var delegate: WWDCAttendesDelegate? { get set }
    func fetchAttendees()
}

UIViewController 子類中的實現(xiàn)是像這樣的:

init(attendeesHandler: WWDCAttendeesHandler) {
 
    self.attendeesHandler = attendeesHandler
    super.init(nibName: nil, bundle: nil)
}
 
override func viewDidLoad() {
    super.viewDidLoad()
 
    atteendeesUIController = WWDCAttendeesUIController(view: view, tableView: tableView)
    attendeesHandler.delegate = atteendeesUIController
 
    attendeesHandler.fetchAttendees()
}

這種實現(xiàn)方式是把請求的操作放在UIViewController 中,把返回數(shù)據(jù)的處理操作放在WWDCAttendeesUIController中:

extension WWDCAttendeesUIController: WWDCAttendesDelegate {
 
    func update(newState: UIState) {
 
        switch(state, newState) {
 
        case (.Loading, .Loading): loadingToLoading()
        case (.Loading, .Success(let attendees)): loadingToSuccess(attendees)
 
        default: fatalError("Not yet implemented \(state) to \(newState)")
        }
    }
 
    func loadingToLoading() {
        view.addSubview(loadingView)
        loadingView.frame = CGRect(origin: .zero, size: view.frame.size)
    }
 
    func loadingToSuccess(attendees: [Attendee]) {
        loadingView.removeFromSuperview()
        tableViewDataSource.dataSource = attendees.map(AttendeeCellController.init)
    }
}

你可以看到 WWDCAttendeesUIController是 UI 的大腦,而WWDCAttendeesController是業(yè)務邏輯的大腦。

看吧,這并不難!但是這個例子引出了一個問題:誰來創(chuàng)建 Controller ?
我建議將 Controller 封裝成可注入的,所以 Controller 應該由你的 UIViewController 來提供。這有兩個主要的好處:

  • 容易測試。你可以傳遞任何遵循FetchNumberOfTickets協(xié)議的對象。
  • Controller 層可以干凈地被解耦。這有助于我們明晰層的責任,使代碼更加健壯。

Model 層

Model 層并不像它看起來那樣不需要解釋。
正如你期望的,這一層的主要組成部分是 model 對象。在票據(jù)的例子中,我們會有根據(jù)票的結(jié)構(gòu)創(chuàng)建 model。

除此之外,Model 層里還有以下組成:

  • 網(wǎng)絡訪問代碼。它們是長這樣的。一般情況下,整個 app 中只有一個類負責網(wǎng)絡訪問活動。
  • 數(shù)據(jù)持久化代碼。你會在這里使用 Core Data 或者簡單的把數(shù)據(jù)轉(zhuǎn)化為 NSData 存儲在磁盤上。
  • 數(shù)據(jù)解析代碼。所有將網(wǎng)絡請求返回數(shù)據(jù)解析為 model 對象的工作都應該在 Model 層完成。
    其中 model 對象是領域特定(domain-specific)的,網(wǎng)絡訪問的代碼是高度可復用的。

Controller 會利用 Model 層里的所有元素來定義 App 中的信息流。

MVC: Massive View Controller?

一些不注意的開發(fā)者會把不屬于 UIViewController 職責的代碼放到 UIViewController 里,結(jié)果就變成了我們所說的 Massive View Controller。越來越多的不相關代碼比如網(wǎng)絡請求和數(shù)據(jù)解析等,最終讓 UIViewController 變得十分臃腫,導致你很難最終信息的流動。更糟糕的是你很難安全地重構(gòu),因為這部分代碼很難寫單元測試。

想要快速找到一個方法去處理 Massive ViewController 是很困難的,所以這往往會變成技術債。這個是 iOS 開發(fā)圈常見的問題,這也是為什么 MVC 模式有些“聲名狼藉”。

但是,活人總不能被尿憋死。

作為經(jīng)驗法則,UIViewController 里的代碼不應該超過 130 行。這似乎很難做到,但是你嚴格地執(zhí)行,還是很容易達到的。下面的幾條指導原則也許可以幫助你:

  • view controller 里的所有代碼應該是跟 root UIView 的行為有關。
  • 它應該負責 root UIView 和 Controller 之間的溝通。比如通過 IBAction 調(diào)用 Controller 里的方法(FetchNumberOfTickets)。
  • UITableDataSource、UITableViewDelegate之類的代理方法也不應該放在這里。如果放在這里,就很難去測試。
  • 如果你認為 view controller 有太多的屬性,可以把它拆分成多個 view controller 或者創(chuàng)建一個自定義的 UIView。

這些僅僅是作為參考。有時候你的 UIViewController 就是很簡單,那么就沒必要把它拆分的那么細。需要記住的是,每當你讓 view controller 承擔新職責,那么就意味著你放棄了對這段代碼的測試和重用。

關于 MVVM

Model-View-ViewModel,所謂的 MVVM,是 MVC 的一個派生,概念上是相似的。它們之間最大的不同是層于層之間的交流方式,并且在 MVVM 中,Controller 被 ViewModel 所取代。

在實踐中,如果配合 FRP 框架進行使用,MVVM 可以大放異彩。因為 Model 被 ViewModel 監(jiān)聽,ViewModel 被 View 所監(jiān)聽,將 FRP 范式用作信息流的管理成為了一個自然而然選擇。 這可以讓層于層之間相互獨立,低耦合度的組件也更容易被測試。

必須要說的是:架構(gòu)當然是重要的,但是正確的編程范式在提高整體的代碼質(zhì)量中扮演著更加重要的角色。少數(shù)情況下我們會在一個 App 中引入不同的架構(gòu)或者編程范式,你可能會覺得這樣做破壞了代碼的統(tǒng)一性,但是如果符合業(yè)務需求也未嘗不可。

更多

MVC 的出現(xiàn)已經(jīng)有很多年歷史了,它也會一直發(fā)展下去。我們不應該讓 MVC 為開發(fā)者的使用不當而背鍋。

MVC 只是一個藍圖,還有很多東西需要開發(fā)者自己去填寫。你可以把 MVC 看作一個食譜,它只是指引你應該怎么做,但是還有仍然有很多東西需要你自己決定。這有利也有弊,弊的是,如果沒有足夠的經(jīng)驗,你可能會繞遠路。利的是,它為你自己的設計預留了靈活的空間。

軟件架構(gòu)沒有新舊之別,它是一顆銀彈。作為開發(fā)者首要關注的是好的工程原則

假如我能給年輕時候的自己提供關于 MVC 的建議,我會告訴他:

  • 首先,要明確每個對象的職責。然后才是思考代碼怎么寫。
  • 不要低估依賴注入。它會給你代碼的重用性和可測試性帶來驚喜。
  • 盡可能避免在 UIViewController 中寫邏輯。UIViewController 越干凈,你就越容易理解它的行為。

我提供了一個小工程來展示本文討論的 MVC 最佳實踐。

如果你遵循這些原則,就能讓 MVC 成為你的朋友而不是敵人。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容