被誤解的 MVC 和被神化的 MVVM

被誤解的 MVC

MVC 的歷史

MVC,全稱是 Model View Controller,是模型 (model)-視圖 (view)-控制器 (controller) 的縮寫。它表示的是一種常見的客戶端軟件開發框架。

經典模式圖

MVC 的概念最早出現在二十世紀八十年代的 施樂帕克 實驗室中(對,就是那個發明圖形用戶界面和鼠標的實驗室),當時施樂帕克為 Smalltalk 發明了這種軟件設計模式。

現在,MVC 已經成為主流的客戶端編程框架,在 iOS 開發中,系統為我們實現好了公共的視圖類:UIView,和控制器類:UIViewController。大多數時候,我們都需要繼承這些類來實現我們的程序邏輯,因此,我們幾乎逃避不開 MVC 這種設計模式。

但是,幾十年過去了,我們對于 MVC 這種設計模式真的用得好嗎?其實不是的,MVC 這種分層方式雖然清楚,但是如果使用不當,很可能讓大量代碼都集中在 Controller 之中,讓 MVC 模式變成了 Massive View Controller 模式。

Controller 的臃腫問題何解?

很多人試圖解決 MVC 這種架構下 Controller 比較臃腫的問題。我還記得半年前 InfoQ 搞了一次移動座談會,當時 BeeFrameworkSamurai-Native 的作者 老郭 問了我一句話:「什么樣的內容才應該放到 Controller 中?」。但是當時因為時間不夠,我沒能展開我的觀點,這次正好在這里好好談談我對于這個問題的想法。

我們來看看 MVC 這種架構的特點。其實設計模式很多時候是為了 Don't repeat yourself 原則來做的,該原則要求能夠復用的代碼要盡量復用,來保證重用。在 MVC 這種設計模式中,我們發現 View 和 Model 都是符合這種原則的。

對于 View 來說,你如果抽象得好,那么一個 App 的動畫效果可以很方便地移植到別的 App 上,而 Github 上也有很多 UI 控件,這些控件都是在 View 層做了很好的封裝設計,使得它能夠方便地開源給大家復用。

對于 Model 來說,它其實是用來存儲業務的數據的,如果做得好,它也可以方便地復用。比如我當時在做有道云筆記 iPad 版的時候,我們就直接和 iOS 版復用了所有的 Model 層的代碼。在創業做猿題庫客戶端時,iOS 和 iPad 版的 Model 層代碼再次被復用上了。當然,因為和業務本身的數據意義相關,Model 層的復用大多數是在一個產品內部,不太可能像 View 層那樣開源給社區。

說完 View 和 Model 了,那我們想想 Controller,Controller 有多少可以復用的?我們寫完了一個 Controller 之后,可以很方便地復用它嗎?結論是:非常難復用。在某些場景下,我們可能可以用 addSubViewController 之類的方式復用 Controller,但它的復用場景還是非常非常少的。

如果我們能夠意識到 Controller 里面的代碼不便于復用,我們就能知道什么代碼應該寫在 Controller 里面了,那就是那些不能復用的代碼。在我看來,Controller 里面就只應該存放這些不能復用的代碼,這些代碼包括:

  • 在初始化時,構造相應的 View 和 Model。
  • 監聽 Model 層的事件,將 Model 層的數據傳遞到 View 層。
  • 監聽 View 層的事件,并且將 View 層的事件轉發到 Model 層。

如果 Controller 只有以上的這些代碼,那么它的邏輯將非常簡單,而且也會非常短。

但是,我們卻很難做到這一點,因為還是有很多邏輯我們不知道寫在哪里,于是就都寫到了 Controller 中了,那我們接下來就看看其它邏輯應該寫在哪里。

如何對 ViewController 瘦身?

objc.io 是一個非常有名的 iOS 開發博客,它上面的第一課 《Lighter View Controllers》 上就講了很多這樣的技巧,我們先總結一下它里面的觀點:

  • 將 UITableView 的 Data Source 分離到另外一個類中。
  • 將數據獲取和轉換的邏輯分別到另外一個類中。
  • 將拼裝控件的邏輯,分離到另外一個類中。

你想明白了嗎?其實 MVC 雖然只有三層,但是它并沒有限制你只能有三層。所以,我們可以將 Controller 里面過于臃腫的邏輯抽取出來,形成新的可復用模塊或架構層次。

我個人對于邏輯的抽取,有以下總結。

將網絡請求抽象到單獨的類中

新手寫代碼,直接就在 Controller 里面用 AFNetworking 發一個請求,請求的完數據直接就傳遞給 View。入門一些的同學,知道把這些請求代碼移到另外一個靜態類里面。但是我覺得還不夠,所以我建議將每一個網絡請求直接封裝成類。

把每一個網絡請求封裝成對象其實是使用了設計模式中的 Command 模式,它有以下好處:

  • 將網絡請求與具體的第三方庫依賴隔離,方便以后更換底層的網絡庫。實際上我們公司的 iOS 客戶端最初是基于
  • ASIHttpRequest
  • 的,我們只花了兩天,就很輕松地切換到了
  • AFNetworking
  • 方便在基類中處理公共邏輯,例如猿題庫的數據版本號信息就統一在基類中處理。
  • 方便在基類中處理緩存邏輯,以及其它一些公共邏輯。
  • 方便做對象的持久化。

大家如果感興趣,可以看我們公司開源的 iOS 網絡庫:YTKNetwork。它在這種思考的指導下,不但將 Controller 中的代碼瘦身,而且進一步演化和加強,現在它還支持諸如復雜網絡請求管理,斷點續傳,插件機制,JSON 合法性檢查等功能。

這部分代碼從 Controller 中剝離出來后,不但簡化了 Controller 中的邏輯,也達到了網絡層的代碼復用的效果。

將界面的拼裝抽象到專門的類中

新手寫代碼,喜歡在 Controller 中把一個個 UILabel ,UIButton,UITextField 往 self.view 上用 addSubView 方法放。我建議大家可以用兩種辦法把這些代碼從 Controller 中剝離。

方法一:構造專門的 UIView 的子類,來負責這些控件的拼裝。這是最徹底和優雅的方式,不過稍微麻煩一些的是,你需要把這些控件的事件回調先接管,再都一一暴露回 Controller。

方法二:用一個靜態的 Util 類,幫助你做 UIView 的拼裝工作。這種方式稍微做得不太徹底,但是比較簡單。

對于一些能復用的 UI 控件,我建議用方法一。如果項目工程比較復雜,我也建議用方法一。如果項目太緊,另外相關項目的代碼量也不多,可以嘗試方法二。

構造 ViewModel

誰說 MVC 就不能用 ViewModel 的?MVVM 的優點我們一樣可以借鑒。具體做法就是將 ViewController 給 View 傳遞數據這個過程,抽象成構造 ViewModel 的過程。

這樣抽象之后,View 只接受 ViewModel,而 Controller 只需要傳遞 ViewModel 這么一行代碼。而另外構造 ViewModel 的過程,我們就可以移動到另外的類中了。

在具體實踐中,我建議大家專門創建構造 ViewModel 工廠類,參見 工廠模式。另外,也可以專門將數據存取都抽將到一個 Service 層,由這層來提供 ViewModel 的獲取。

專門構造存儲類

剛剛說到 ViewModel 的構造可以抽獎到一個 Service 層。與此相應的,數據的存儲也應該由專門的對象來做。在小猿搜題項目中,我們由一個叫 UserAgent 的類,專門來處理本地數據的存取。

數據存取放在專門的類中,就可以針對存取做額外的事情了。比如:

  • 對一些熱點數據增加緩存
  • 處理數據遷移相關的邏輯

如果要做得更細,可以把存儲引擎再抽象出一層。這樣你就可以方便地切換存儲的底層,例如從 sqlite 切換到 key-value 的存儲引擎等。

小結

通過代碼的抽取,我們可以將原本的 MVC 設計模式中的 ViewController 進一步拆分,構造出 網絡請求層、ViewModel 層、Service 層、Storage 層等其它類,來配合 Controller 工作,從而使 Controller 更加簡單,我們的 App 更容易維護。

另外,不知道大家注意到沒,其實 Controller 層是非常難于測試的,如果我們能夠將 Controller 瘦身,就可以更方便地寫 Unit Test 來測試各種與界面的無關的邏輯。移動端自動化測試框架都不太成熟,但是將 Controller 的代碼抽取出來,是有助于我們做測試工作的。

MVVM 是 Model-View-ViewModel 的簡寫。

MVVM

相對于 MVC 的歷史來說,MVVM 是一個相當新的架構,MVVM 最早于 2005 年被微軟的 WPF 和 Silverlight 的架構師 John Gossman 提出,并且應用在微軟的軟件開發中。當時 MVC 已經被提出了 20 多年了,可見兩者出現的年代差別有多大。

MVVM 在使用當中,通常還會利用雙向綁定技術,使得 Model 變化時,ViewModel 會自動更新,而 ViewModel 變化時,View 也會自動變化。所以,MVVM 模式有些時候又被稱作:model-view-binder 模式。

具體在 iOS 中,可以使用 KVO 或 Notification 技術達到這種效果。

MVVM 的神化

在使用中,我發現大家對于 MVVM 以及 MVVM 衍生出來的框架(比如 ReactiveCocoa)有一種「敬畏」感。這種「敬畏」感某種程度上就像對神一樣,這主要表現在我沒有聽到大家對于 MVVM 的任何批評。

我感覺原因首先是 MVVM 并沒有很大程度上普及,大家對于新技術一般都不熟,進而不敢妄加評論。另外,ReactiveCocoa 本身上手的復雜性,也讓很多人感覺到這種技術很高深難懂,進而加重了大家對它的「敬畏」。

MVVM 的作用和問題

MVVM 在實際使用中,確實能夠使得 Model 層和 View 層解耦,但是如果你需要實現 MVVM 中的雙向綁定的話,那么通常就需要引入更多復雜的框架來實現了。

對此,MVVM 的作者 John Gossman 的 批評 應該是最為中肯的。John Gossman 對 MVVM 的批評主要有兩點:

第一點:數據綁定使得 Bug 很難被調試。你看到界面異常了,有可能是你 View 的代碼有 Bug,也可能是 Model 的代碼有問題。數據綁定使得一個位置的 Bug 被快速傳遞到別的位置,要定位原始出問題的地方就變得不那么容易了。

第二點:對于過大的項目,數據綁定需要花費更多的內存。

某種意義上來說,我認為就是數據綁定使得 MVVM 變得復雜和難用了。但是,這個缺點同時也被很多人認為是優點。

ReactiveCocoa

函數式編程(Functional Programming)和響應式編程(React Programming)也是當前很火的兩個概念,它們的結合可以很方便地實現數據的綁定。于是,在 iOS 編程中,ReactiveCocoa 橫空出世了,它的概念都非常 新,包括:

  • 函數式編程(Functional Programming),函數也變成一等公民了,可以擁有和對象同樣的功能,例如當成參數傳遞,當作返回值等。看看 Swift 語言帶來的眾多函數式編程的特性,就你知道這多 Cool 了。
  • 響應式編程(React Programming),原來我們基于事件(Event)的處理方式都弱了,現在是基于輸入(在 ReactiveCocoa 里叫 Signal)的處理方式。輸入還可以通過函數式編程進行各種 Combine 或 Filter,盡顯各種靈活的處理。
  • 無狀態(Stateless),狀態是函數的魔鬼,無狀態使得函數能更好地測試。
  • 不可修改(Immutable),數據都是不可修改的,使得軟件邏輯簡單,也可以更好地測試。

哇,所有這些都太 Cool 了。當我看到的時候,我都雞凍了!

我們應該客觀評價 MVVM 和 ReactiveCocoa

但是但是,我突然想到,我好象只需要一個 ViewModel 而已,我完全可以簡單地做一個 ViewModel 的工廠類或 Service 類就可以了,為什么要引入這么多框架?現有的 MVC 真的有那么大的問題嗎?

直到現在,ReactiveCocoa 在國內外還都是在小眾領域,沒有被大量接受成為主流的編程框架。不只是在 iOS 語言,在別的語言中,例如 Java 中的 RxJava 也同樣沒有成為主流。

我在這里,不是想說 ReactiveCocoa 不好,也不是想說 MVVM 不好,而是想讓大家都能夠有一個客觀的認識。ReactiveCocoa 和 MVVM 不應該被神化,它是一種新穎的編程框架,能夠解決舊有編程框架的一些問題,但是也會帶來一些新問題,僅此而已。如果不能使好的駕馭 ReactiveCocoa,同樣會造成 Controller 代碼過于復雜,代碼邏輯不易維護的問題。

總結

有一些人總是追趕著技術,有什么新技術不管三七二十一立馬就用,結果被各種坑。

又有一些人,總是擔心新技術帶來的技術風險,不愿意學習。結果現在還有人在用 MRC 手動管理引用計數。

而我想說,我們需要保持的是一個擁抱變化的心,以及理性分析的態度。在新技術的面前,不盲從,也不守舊,一切的決策都應該建立在認真分析的基礎上,這樣才能應對技術的變化。
附上MVVMDemo 便于理解

原文作者:唐巧
原文鏈接:https://blog.devtang.com/2015/11/02/mvc-and-mvvm/
發表日期:2015.11.02 , 6:06 AM

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,316評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,481評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,241評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,939評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,697評論 6 409
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,182評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,247評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,406評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,933評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,772評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,973評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,516評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,638評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,866評論 1 285
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,644評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,953評論 2 373

推薦閱讀更多精彩內容