本文翻譯自《Model-View-Controller (MVC) in iOS: A Modern Approach》
每一位剛?cè)腴T的 iOS 開發(fā)者都會接觸到大量的知識信息岛啸,這些信息對我們學(xué)習(xí)一門新語言钓觉、新框架包括蘋果推薦的 MVC 設(shè)計模式來說非常重要。
要跟上 iOS 發(fā)展的腳步是一件不容易的事坚踩,很多時候開發(fā)者并沒有對 MVC 引起足夠的重視荡灾,然而很多問題確是由此導(dǎo)致的。
這篇文章會幫助你繞開 MVC 實踐中常見的陷阱瞬铸。你可以學(xué)習(xí)到一種更現(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)絡(luò)請求的代碼都會放在這里豆瘫。
- View:用戶直接交互的地方珊蟀。這里的類基本都是可以復(fù)用的,這些類里沒有特殊的邏輯外驱。比如育灸,
UILabel
就是把文本展示到屏幕上,并且它很容易被重用略步。 - Controller:Model 和 View 的中介描扯,比較典型的是我們會在這里使用代理模式定页。在理想的情況下 View 對 Controller 來說是透明的趟薄。Controller 會通過一個抽象比如協(xié)議來和 View 進(jìn)行交流,就像
UITableView
通過UITableViewDataSource
來和它的數(shù)據(jù)源進(jìn)行交流一樣典徊。
當(dāng)你把這些放在一起時候杭煎,它應(yīng)該是這樣的:
是不是很簡單呢?
但是常言道:細(xì)節(jié)決定成敗卒落。只有當(dāng)你真正實踐 MVC 的時候才會發(fā)現(xiàn)事情并不是想象中那么容易羡铲。
蘋果官方的 MVC 文檔 對 MVC 有詳細(xì)的闡述,這會讓你對 MVC 有一個系統(tǒng)的理論了解儡毕,幫助你避免潛在的問題也切。
但是扑媚,僅僅有理論是遠(yuǎn)遠(yuǎn)不夠,實踐才是檢驗真理的唯一標(biāo)準(zhǔn)雷恃。
MVC 的最佳實踐
雖然 MVC 的理論比較容易理解疆股,但是在實踐的過程中我們還是會遇到很多棘手的問題。讓我們來著手解決這些問題吧倒槐。
View 層
當(dāng)用戶使用你的 App 時旬痹,他們大部分時間就是在和 View 層打交道。View 層應(yīng)該是 App 中最直白的部分讨越,因為它不包含任何業(yè)務(wù)邏輯两残。在代碼層面,你通嘲芽纾可以在這一層看到:
-
UIView
的子類們人弓。從最基本的UIView
到復(fù)雜的 UI 控件。 - 一個
UIViewController
(可以論證的)着逐。我個人認(rèn)為它應(yīng)該屬于這一層票从,因為UIViewController
和其根UIView
以及它的生命周期(loadView
,viewDidLoad
) 是密不可分的。當(dāng)然不是所有人都同意滨嘱。 -
UIViewController
的 animations 和 transitions峰鄙。 -
UIKit/AppKit
、Core Animation
和Core Graphics
中的部分類太雨。
這一層的代碼異味(Code smell)可能有多種表現(xiàn)形式吟榴,但是總的來說就是在 View 層做了和 UI 不相關(guān)的事。一個典型的代碼異味就是在 UIViewController
中做網(wǎng)絡(luò)請求囊扳。
為了趕 deadline吩翻,往 UIViewController
中扔一堆代碼是一件很誘惑人的事。最好別這樣做锥咸,也許這在當(dāng)下能給你節(jié)省幾分鐘時間狭瞎,但是以后,你可能會為了找一個 bug 而花費幾個小時搏予,或者當(dāng)你想在另一個 view controller
中重用這段代碼的時候發(fā)現(xiàn)這很困難熊锭。
把你的 View 層和下面的清單進(jìn)行核對:
- 它是否和 Model 層進(jìn)行交互?
- 它是否包含任何業(yè)務(wù)邏輯雪侥?
- 它是否做了一些和 UI 不相關(guān)的事碗殷?
如果滿足上述任何一個條件,那么是時候?qū)δ愕?View 層進(jìn)行清理和重構(gòu)了速缨。
當(dāng)然锌妻,這些規(guī)則不是鐵的定律,有時候由于各種原因你不等不違背旬牲。盡管如此仿粹,對它們抱以尊重還是很有必要的搁吓。
最后,如果你把這些類寫得很好吭历,你總是可以重用它們擎浴。如果你不相信我,就看看 GitHub 上 UI 組件的數(shù)量吧毒涧。
Controller 層
Controller 層是你的 App 中最少重用的部分贮预,因為這里面包含很多特定的邏輯。這并不奇怪契讲,有些東西在你的 App 里有用仿吞,但是對其它 App 來說沒有任何用處。
通常捡偏,在這一層中你會思考這些問題:
- 先訪問持久化數(shù)據(jù)還是網(wǎng)絡(luò)數(shù)據(jù)唤冈?
- 多久刷新一次 App?
- 頁面的在不同的條件下應(yīng)該如何跳轉(zhuǎn)银伟?
- 當(dāng) App 進(jìn)入后臺的時候你虹,哪些需要被清理?
你應(yīng)該把 Controller 層當(dāng)作 App 的大腦:它決定了下一步會發(fā)生什么彤避。你會經(jīng)常測試這些類傅物,以確保一切都是如期運行。
舉個例子
現(xiàn)在你應(yīng)該對 Controller 層有了更好的認(rèn)識琉预,讓我們來看一個簡單例子董饰。
提示:如果你想了解這在 App 環(huán)境下是如何工作的,可以下載我為你準(zhǔn)備的簡單 App圆米。
想象一下你有一個 UIViewController
的子類卒暂,它想知道參加今年 WWDC 的人員名單。為了達(dá)到這個目的也祠,它會利用一個 controller
類。因為蘋果推薦我們應(yīng)該重視從一個協(xié)議開始近速,所以我們會這么做:
enum UIState {
case Loading
case Success([Attendee])
case Failure(Error)
}
protocol WWDCAttendesDelegate: class {
var state: UIState { get set}
}
我們先將 state
初始化為 Loading
, 然后當(dāng)參加 WWDC 的人員名單加載成功(或者失斦┖佟)的時候更新 state
值。
因為我們不希望在 UIViewController 中處理返回數(shù)據(jù)永淌,所以用一個單獨的對象(WWDCAttendeesUIController
)來實現(xiàn)WWDCAttendesDelegate
佩耳。這樣分離的操作可以讓我們對 WWDCAttendeesUIController
進(jìn)行獨立的測試干厚。
下一步就是為 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è)務(wù)邏輯的大腦闲先。
看吧状土,這并不難!但是這個例子引出了一個問題:誰來創(chuàng)建 Controller 伺糠?
我建議將 Controller 封裝成可注入的蒙谓,所以 Controller 應(yīng)該由你的 UIViewController
來提供。這有兩個主要的好處:
- 容易測試训桶。你可以傳遞任何遵循
FetchNumberOfTickets
協(xié)議的對象累驮。 - Controller 層可以干凈地被解耦。這有助于我們明晰層的責(zé)任舵揭,使代碼更加健壯谤专。
Model 層
Model 層并不像它看起來那樣不需要解釋。
正如你期望的午绳,這一層的主要組成部分是 model
對象毒租。在票據(jù)的例子中,我們會有根據(jù)票的結(jié)構(gòu)創(chuàng)建 model
箱叁。
除此之外墅垮,Model 層里還有以下組成:
- 網(wǎng)絡(luò)訪問代碼。它們是長這樣的耕漱。一般情況下算色,整個 app 中只有一個類負(fù)責(zé)網(wǎng)絡(luò)訪問活動。
- 數(shù)據(jù)持久化代碼螟够。你會在這里使用 Core Data 或者簡單的把數(shù)據(jù)轉(zhuǎn)化為
NSData
存儲在磁盤上灾梦。 - 數(shù)據(jù)解析代碼。所有將網(wǎng)絡(luò)請求返回數(shù)據(jù)解析為
model
對象的工作都應(yīng)該在 Model 層完成妓笙。
其中model
對象是領(lǐng)域特定(domain-specific)的若河,網(wǎng)絡(luò)訪問的代碼是高度可復(fù)用的。
Controller 會利用 Model 層里的所有元素來定義 App 中的信息流寞宫。
MVC: Massive View Controller?
一些不注意的開發(fā)者會把不屬于 UIViewController
職責(zé)的代碼放到 UIViewController
里萧福,結(jié)果就變成了我們所說的 Massive View Controller。越來越多的不相關(guān)代碼比如網(wǎng)絡(luò)請求和數(shù)據(jù)解析等辈赋,最終讓 UIViewController
變得十分臃腫鲫忍,導(dǎo)致你很難最終信息的流動膏燕。更糟糕的是你很難安全地重構(gòu),因為這部分代碼很難寫單元測試悟民。
想要快速找到一個方法去處理 Massive ViewController 是很困難的坝辫,所以這往往會變成技術(shù)債。這個是 iOS 開發(fā)圈常見的問題射亏,這也是為什么 MVC 模式有些“聲名狼藉”近忙。
但是,活人總不能被尿憋死智润。
作為經(jīng)驗法則银锻,UIViewController
里的代碼不應(yīng)該超過 130 行。這似乎很難做到做鹰,但是你嚴(yán)格地執(zhí)行击纬,還是很容易達(dá)到的。下面的幾條指導(dǎo)原則也許可以幫助你:
-
view controller
里的所有代碼應(yīng)該是跟 rootUIView
的行為有關(guān)钾麸。 - 它應(yīng)該負(fù)責(zé) root
UIView
和 Controller 之間的溝通更振。比如通過IBAction
調(diào)用 Controller 里的方法(FetchNumberOfTickets
)。 -
UITableDataSource
饭尝、UITableViewDelegate
之類的代理方法也不應(yīng)該放在這里肯腕。如果放在這里,就很難去測試钥平。 - 如果你認(rèn)為
view controller
有太多的屬性实撒,可以把它拆分成多個view controller
或者創(chuàng)建一個自定義的UIView
。
這些僅僅是作為參考涉瘾。有時候你的 UIViewController
就是很簡單知态,那么就沒必要把它拆分的那么細(xì)。需要記住的是立叛,每當(dāng)你讓 view controller
承擔(dān)新職責(zé)负敏,那么就意味著你放棄了對這段代碼的測試和重用。
關(guān)于 MVVM
Model-View-ViewModel秘蛇,所謂的 MVVM其做,是 MVC 的一個派生,概念上是相似的赁还。它們之間最大的不同是層于層之間的交流方式妖泄,并且在 MVVM 中,Controller 被 ViewModel 所取代艘策。
在實踐中蹈胡,如果配合 FRP 框架進(jìn)行使用,MVVM 可以大放異彩。因為 Model 被 ViewModel 監(jiān)聽审残,ViewModel 被 View 所監(jiān)聽梭域,將 FRP 范式用作信息流的管理成為了一個自然而然選擇斑举。 這可以讓層于層之間相互獨立搅轿,低耦合度的組件也更容易被測試。
必須要說的是:架構(gòu)當(dāng)然是重要的富玷,但是正確的編程范式在提高整體的代碼質(zhì)量中扮演著更加重要的角色璧坟。少數(shù)情況下我們會在一個 App 中引入不同的架構(gòu)或者編程范式,你可能會覺得這樣做破壞了代碼的統(tǒng)一性赎懦,但是如果符合業(yè)務(wù)需求也未嘗不可雀鹃。
更多
MVC 的出現(xiàn)已經(jīng)有很多年歷史了,它也會一直發(fā)展下去励两。我們不應(yīng)該讓 MVC 為開發(fā)者的使用不當(dāng)而背鍋黎茎。
MVC 只是一個藍(lán)圖,還有很多東西需要開發(fā)者自己去填寫当悔。你可以把 MVC 看作一個食譜傅瞻,它只是指引你應(yīng)該怎么做,但是還有仍然有很多東西需要你自己決定盲憎。這有利也有弊嗅骄,弊的是,如果沒有足夠的經(jīng)驗饼疙,你可能會繞遠(yuǎn)路溺森。利的是,它為你自己的設(shè)計預(yù)留了靈活的空間窑眯。
軟件架構(gòu)沒有新舊之別屏积,它是一顆銀彈。作為開發(fā)者首要關(guān)注的是好的工程原則磅甩。
假如我能給年輕時候的自己提供關(guān)于 MVC 的建議肾请,我會告訴他:
- 首先,要明確每個對象的職責(zé)更胖。然后才是思考代碼怎么寫铛铁。
- 不要低估依賴注入。它會給你代碼的重用性和可測試性帶來驚喜却妨。
- 盡可能避免在
UIViewController
中寫邏輯饵逐。UIViewController
越干凈,你就越容易理解它的行為彪标。
我提供了一個小工程來展示本文討論的 MVC 最佳實踐倍权。
如果你遵循這些原則,就能讓 MVC 成為你的朋友而不是敵人。