MVC 在 iOS 中的最佳實踐

本文翻譯自《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/AppKitCore AnimationCore 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)該是跟 root UIView 的行為有關(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 成為你的朋友而不是敵人。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末薄声,一起剝皮案震驚了整個濱河市当船,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌默辨,老刑警劉巖德频,帶你破解...
    沈念sama閱讀 221,576評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異缩幸,居然都是意外死亡壹置,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評論 3 399
  • 文/潘曉璐 我一進(jìn)店門表谊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來钞护,“玉大人,你說我怎么就攤上這事爆办∧压荆” “怎么了?”我有些...
    開封第一講書人閱讀 168,017評論 0 360
  • 文/不壞的土叔 我叫張陵距辆,是天一觀的道長余佃。 經(jīng)常有香客問我,道長挑格,這世上最難降的妖魔是什么咙冗? 我笑而不...
    開封第一講書人閱讀 59,626評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮漂彤,結(jié)果婚禮上雾消,老公的妹妹穿的比我還像新娘。我一直安慰自己挫望,他們只是感情好立润,可當(dāng)我...
    茶點故事閱讀 68,625評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著媳板,像睡著了一般桑腮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蛉幸,一...
    開封第一講書人閱讀 52,255評論 1 308
  • 那天破讨,我揣著相機(jī)與錄音,去河邊找鬼奕纫。 笑死提陶,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的匹层。 我是一名探鬼主播隙笆,決...
    沈念sama閱讀 40,825評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了撑柔?” 一聲冷哼從身側(cè)響起瘸爽,我...
    開封第一講書人閱讀 39,729評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎铅忿,沒想到半個月后剪决,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,271評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡辆沦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,363評論 3 340
  • 正文 我和宋清朗相戀三年昼捍,在試婚紗的時候發(fā)現(xiàn)自己被綠了识虚。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肢扯。...
    茶點故事閱讀 40,498評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖担锤,靈堂內(nèi)的尸體忽然破棺而出蔚晨,到底是詐尸還是另有隱情,我是刑警寧澤肛循,帶...
    沈念sama閱讀 36,183評論 5 350
  • 正文 年R本政府宣布铭腕,位于F島的核電站,受9級特大地震影響多糠,放射性物質(zhì)發(fā)生泄漏累舷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,867評論 3 333
  • 文/蒙蒙 一夹孔、第九天 我趴在偏房一處隱蔽的房頂上張望被盈。 院中可真熱鬧,春花似錦搭伤、人聲如沸只怎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,338評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽身堡。三九已至,卻和暖如春拍鲤,著一層夾襖步出監(jiān)牢的瞬間贴谎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,458評論 1 272
  • 我被黑心中介騙來泰國打工季稳, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留擅这,地道東北人。 一個月前我還...
    沈念sama閱讀 48,906評論 3 376
  • 正文 我出身青樓绞幌,卻偏偏與公主長得像蕾哟,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,507評論 2 359

推薦閱讀更多精彩內(nèi)容