被誤解的MVC和被神化的MVVM(作者:唐巧)

文章轉(zhuǎn)自: http://www.infoq.com/cn/articles/rethinking-mvc-mvvm

作者 唐巧 發(fā)布于 2015年11月1日 |

被誤解的 MVC


MVC 的歷史

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

MVC 的概念最早出現(xiàn)在二十世紀(jì)八十年代的 施樂帕克 實(shí)驗(yàn)室中(對鲸阻,就是那個(gè)發(fā)明圖形用戶界面和鼠標(biāo)的實(shí)驗(yàn)室)六荒,當(dāng)時(shí)施樂帕克為 Smalltalk 發(fā)明了這種軟件設(shè)計(jì)模式养距。

現(xiàn)在尤筐,MVC 已經(jīng)成為主流的客戶端編程框架汇荐,在 iOS 開發(fā)中洞就,系統(tǒng)為我們實(shí)現(xiàn)好了公共的視圖類:UIView,和控制器類:UIViewController拢驾。大多數(shù)時(shí)候奖磁,我們都需要繼承這些類來實(shí)現(xiàn)我們的程序邏輯,因此繁疤,我們幾乎逃避不開 MVC 這種設(shè)計(jì)模式咖为。

但是,幾十年過去了稠腊,我們對于 MVC 這種設(shè)計(jì)模式真的用得好嗎躁染?其實(shí)不是的,MVC 這種分層方式雖然清楚架忌,但是如果使用不當(dāng)吞彤,很可能讓大量代碼都集中在 Controller 之中,讓 MVC 模式變成了 Massive View Controller 模式叹放。

Controller 的臃腫問題何解饰恕?

很多人試圖解決 MVC 這種架構(gòu)下 Controller 比較臃腫的問題。我還記得半年前 InfoQ 搞了一次移動座談會井仰,當(dāng)時(shí) BeeFrameworkSamurai-Native 的作者 老郭 問了我一句話:「什么樣的內(nèi)容才應(yīng)該放到 Controller 中埋嵌?」。但是當(dāng)時(shí)因?yàn)闀r(shí)間不夠俱恶,我沒能展開我的觀點(diǎn)雹嗦,這次正好在這里好好談?wù)勎覍τ谶@個(gè)問題的想法。

我們來看看 MVC 這種架構(gòu)的特點(diǎn)合是。其實(shí)設(shè)計(jì)模式很多時(shí)候是為了 Don't repeat yourself 原則來做的了罪,該原則要求能夠復(fù)用的代碼要盡量復(fù)用,來保證重用聪全。在 MVC 這種設(shè)計(jì)模式中泊藕,我們發(fā)現(xiàn) View 和 Model 都是符合這種原則的。

對于 View 來說难礼,你如果抽象得好吱七,那么一個(gè) App 的動畫效果可以很方便地移植到別的 App 上,而 Github 上也有很多 UI 控件鹤竭,這些控件都是在 View 層做了很好的封裝設(shè)計(jì)踊餐,使得它能夠方便地開源給大家復(fù)用。

對于 Model 來說臀稚,它其實(shí)是用來存儲業(yè)務(wù)的數(shù)據(jù)的吝岭,如果做得好,它也可以方便地復(fù)用。比如我當(dāng)時(shí)在做有道云筆記 iPad 版的時(shí)候窜管,我們就直接和 iOS 版復(fù)用了所有的 Model 層的代碼散劫。在創(chuàng)業(yè)做猿題庫客戶端時(shí),iOS 和 iPad 版的 Model 層代碼再次被復(fù)用上了幕帆。當(dāng)然获搏,因?yàn)楹蜆I(yè)務(wù)本身的數(shù)據(jù)意義相關(guān),Model 層的復(fù)用大多數(shù)是在一個(gè)產(chǎn)品內(nèi)部失乾,不太可能像 View 層那樣開源給社區(qū)常熙。

說完 View 和 Model 了,那我們想想 Controller碱茁,Controller 有多少可以復(fù)用的裸卫?我們寫完了一個(gè) Controller 之后,可以很方便地復(fù)用它嗎纽竣?結(jié)論是:非常難復(fù)用墓贿。在某些場景下,我們可能可以用addSubViewController 之類的方式復(fù)用 Controller蜓氨,但它的復(fù)用場景還是非常非常少的聋袋。

如果我們能夠意識到 Controller 里面的代碼不便于復(fù)用,我們就能知道什么代碼應(yīng)該寫在 Controller 里面了穴吹,那就是那些不能復(fù)用的代碼幽勒。在我看來,Controller 里面就只應(yīng)該存放這些不能復(fù)用的代碼刀荒,這些代碼包括:

  • 在初始化時(shí),構(gòu)造相應(yīng)的 View 和 Model棘钞。
  • 監(jiān)聽 Model 層的事件缠借,將 Model 層的數(shù)據(jù)傳遞到 View 層。
  • 監(jiān)聽 View 層的事件宜猜,并且將 View 層的事件轉(zhuǎn)發(fā)到 Model 層泼返。

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

但是,我們卻很難做到這一點(diǎn)叫乌,因?yàn)檫€是有很多邏輯我們不知道寫在哪里柴罐,于是就都寫到了 Controller 中了,那我們接下來就看看其它邏輯應(yīng)該寫在哪里憨奸。

如何對 ViewController 瘦身革屠?

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

  • 將 UITableView 的 Data Source 分離到另外一個(gè)類中似芝。
  • 將數(shù)據(jù)獲取和轉(zhuǎn)換的邏輯分別到另外一個(gè)類中那婉。
  • 將拼裝控件的邏輯,分離到另外一個(gè)類中党瓮。

你想明白了嗎详炬?其實(shí) MVC 雖然只有三層,但是它并沒有限制你只能有三層寞奸。所以呛谜,我們可以將 Controller 里面過于臃腫的邏輯抽取出來,形成新的可復(fù)用模塊或架構(gòu)層次蝇闭。

我個(gè)人對于邏輯的抽取呻率,有以下總結(jié)。

將網(wǎng)絡(luò)請求抽象到單獨(dú)的類中

新手寫代碼呻引,直接就在 Controller 里面用 AFNetworking 發(fā)一個(gè)請求礼仗,請求的完數(shù)據(jù)直接就傳遞給 View。入門一些的同學(xué)逻悠,知道把這些請求代碼移到另外一個(gè)靜態(tài)類里面元践。但是我覺得還不夠,所以我建議將每一個(gè)網(wǎng)絡(luò)請求直接封裝成類童谒。

把每一個(gè)網(wǎng)絡(luò)請求封裝成對象其實(shí)是使用了設(shè)計(jì)模式中的 Command 模式单旁,它有以下好處:

  • 將網(wǎng)絡(luò)請求與具體的第三方庫依賴隔離,方便以后更換底層的網(wǎng)絡(luò)庫饥伊。實(shí)際上我們公司的 iOS 客戶端最初是基于 ASIHttpRequest 的象浑,我們只花了兩天,就很輕松地切換到了 AFNetworking琅豆。
  • 方便在基類中處理公共邏輯愉豺,例如猿題庫的數(shù)據(jù)版本號信息就統(tǒng)一在基類中處理。
  • 方便在基類中處理緩存邏輯茫因,以及其它一些公共邏輯蚪拦。
  • 方便做對象的持久化。

大家如果感興趣冻押,可以看我們公司開源的 iOS 網(wǎng)絡(luò)庫:YTKNetwork驰贷。它在這種思考的指導(dǎo)下,不但將 Controller 中的代碼瘦身洛巢,而且進(jìn)一步演化和加強(qiáng)括袒,現(xiàn)在它還支持諸如復(fù)雜網(wǎng)絡(luò)請求管理,斷點(diǎn)續(xù)傳稿茉,插件機(jī)制箱熬,JSON 合法性檢查等功能类垦。

這部分代碼從 Controller 中剝離出來后,不但簡化了 Controller 中的邏輯城须,也達(dá)到了網(wǎng)絡(luò)層的代碼復(fù)用的效果蚤认。

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

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

方法一:構(gòu)造專門的 UIView 的子類陪汽,來負(fù)責(zé)這些控件的拼裝。這是最徹底和優(yōu)雅的方式褥蚯,不過稍微麻煩一些的是挚冤,你需要把這些控件的事件回調(diào)先接管,再都一一暴露回 Controller赞庶。

方法二:用一個(gè)靜態(tài)的 Util 類训挡,幫助你做 UIView 的拼裝工作。這種方式稍微做得不太徹底歧强,但是比較簡單澜薄。

對于一些能復(fù)用的 UI 控件,我建議用方法一摊册。如果項(xiàng)目工程比較復(fù)雜肤京,我也建議用方法一。如果項(xiàng)目太緊茅特,另外相關(guān)項(xiàng)目的代碼量也不多忘分,可以嘗試方法二。

構(gòu)造 ViewModel

誰說 MVC 就不能用 ViewModel 的白修?MVVM 的優(yōu)點(diǎn)我們一樣可以借鑒妒峦。具體做法就是將 ViewController 給 View 傳遞數(shù)據(jù)這個(gè)過程,抽象成構(gòu)造 ViewModel 的過程熬荆。

這樣抽象之后舟山,View 只接受 ViewModel绸狐,而 Controller 只需要傳遞 ViewModel 這么一行代碼卤恳。而另外構(gòu)造 ViewModel 的過程,我們就可以移動到另外的類中了寒矿。

在具體實(shí)踐中突琳,我建議大家專門創(chuàng)建構(gòu)造 ViewModel 工廠類,參見 工廠模式符相。另外拆融,也可以專門將數(shù)據(jù)存取都抽將到一個(gè) Service 層蠢琳,由這層來提供 ViewModel 的獲取。

專門構(gòu)造存儲類

剛剛說到 ViewModel 的構(gòu)造可以抽獎到一個(gè) Service 層镜豹。與此相應(yīng)的傲须,數(shù)據(jù)的存儲也應(yīng)該由專門的對象來做。在小猿搜題項(xiàng)目中趟脂,我們由一個(gè)叫 UserAgent 的類泰讽,專門來處理本地?cái)?shù)據(jù)的存取。

數(shù)據(jù)存取放在專門的類中昔期,就可以針對存取做額外的事情了已卸。比如:

  • 對一些熱點(diǎn)數(shù)據(jù)增加緩存
  • 處理數(shù)據(jù)遷移相關(guān)的邏輯

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

小結(jié)

通過代碼的抽取般贼,我們可以將原本的 MVC 設(shè)計(jì)模式中的 ViewController 進(jìn)一步拆分愧哟,構(gòu)造出 網(wǎng)絡(luò)請求層、ViewModel 層具伍、Service 層翅雏、Storage 層等其它類,來配合 Controller 工作人芽,從而使 Controller 更加簡單望几,我們的 App 更容易維護(hù)。

另外萤厅,不知道大家注意到?jīng)]橄抹,其實(shí) Controller 層是非常難于測試的,如果我們能夠?qū)?Controller 瘦身惕味,就可以更方便地寫 Unit Test 來測試各種與界面的無關(guān)的邏輯楼誓。移動端自動化測試框架都不太成熟,但是將 Controller 的代碼抽取出來名挥,是有助于我們做測試工作的疟羹。

希望本文能幫助大家掌握正確使用 MVC 的姿勢,在下一節(jié)里禀倔,我將分享一下我對 MVVM 的看法榄融。

被神化的 MVVM


MVVM 的歷史

MVVM 是 Model-View-ViewModel 的簡寫。

相對于 MVC 的歷史來說救湖,MVVM 是一個(gè)相當(dāng)新的架構(gòu)愧杯,MVVM 最早于 2005 年被微軟的 WPF 和 Silverlight 的架構(gòu)師 John Gossman 提出,并且應(yīng)用在微軟的軟件開發(fā)中鞋既。當(dāng)時(shí) MVC 已經(jīng)被提出了 20 多年了力九,可見兩者出現(xiàn)的年代差別有多大耍铜。

MVVM 在使用當(dāng)中,通常還會利用雙向綁定技術(shù)跌前,使得 Model 變化時(shí)棕兼,ViewModel 會自動更新,而 ViewModel 變化時(shí)抵乓,View 也會自動變化程储。所以,MVVM 模式有些時(shí)候又被稱作:model-view-binder 模式臂寝。

具體在 iOS 中章鲤,可以使用 KVO 或 Notification 技術(shù)達(dá)到這種效果。

MVVM 的神化

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

我感覺原因首先是 MVVM 并沒有很大程度上普及皱蹦,大家對于新技術(shù)一般都不熟,進(jìn)而不敢妄加評論眷蜈。另外沪哺,ReactiveCocoa 本身上手的復(fù)雜性,也讓很多人感覺到這種技術(shù)很高深難懂酌儒,進(jìn)而加重了大家對它的「敬畏」辜妓。

MVVM 的作用和問題

MVVM 在實(shí)際使用中,確實(shí)能夠使得 Model 層和 View 層解耦忌怎,但是如果你需要實(shí)現(xiàn) MVVM 中的雙向綁定的話籍滴,那么通常就需要引入更多復(fù)雜的框架來實(shí)現(xiàn)了。

對此榴啸,MVVM 的作者 John Gossman 的 批評 應(yīng)該是最為中肯的孽惰。John Gossman 對 MVVM 的批評主要有兩點(diǎn):

第一點(diǎn):數(shù)據(jù)綁定使得 Bug 很難被調(diào)試。你看到界面異常了鸥印,有可能是你 View 的代碼有 Bug勋功,也可能是 Model 的代碼有問題。數(shù)據(jù)綁定使得一個(gè)位置的 Bug 被快速傳遞到別的位置库说,要定位原始出問題的地方就變得不那么容易了狂鞋。

第二點(diǎn):對于過大的項(xiàng)目,數(shù)據(jù)綁定需要花費(fèi)更多的內(nèi)存璃弄。

某種意義上來說要销,我認(rèn)為就是數(shù)據(jù)綁定使得 MVVM 變得復(fù)雜和難用了构回。但是夏块,這個(gè)缺點(diǎn)同時(shí)也被很多人認(rèn)為是優(yōu)點(diǎn)疏咐。

ReactiveCocoa

函數(shù)式編程(Functional Programming)和響應(yīng)式編程(React Programming)也是當(dāng)前很火的兩個(gè)概念,它們的結(jié)合可以很方便地實(shí)現(xiàn)數(shù)據(jù)的綁定脐供。于是浑塞,在 iOS 編程中,ReactiveCocoa 橫空出世了政己,它的概念都非常 新酌壕,包括:

  • 函數(shù)式編程(Functional Programming),函數(shù)也變成一等公民了歇由,可以擁有和對象同樣的功能卵牍,例如當(dāng)成參數(shù)傳遞,當(dāng)作返回值等沦泌『迹看看 Swift 語言帶來的眾多函數(shù)式編程的特性,就你知道這多 Cool 了谢谦。
  • 響應(yīng)式編程(React Programming)释牺,原來我們基于事件(Event)的處理方式都弱了,現(xiàn)在是基于輸入(在 ReactiveCocoa 里叫 Signal)的處理方式回挽。輸入還可以通過函數(shù)式編程進(jìn)行各種 Combine 或 Filter没咙,盡顯各種靈活的處理。
  • 無狀態(tài)(Stateless)千劈,狀態(tài)是函數(shù)的魔鬼祭刚,無狀態(tài)使得函數(shù)能更好地測試。
  • 不可修改(Immutable)墙牌,數(shù)據(jù)都是不可修改的袁梗,使得軟件邏輯簡單,也可以更好地測試憔古。

哇遮怜,所有這些都太 Cool 了。當(dāng)我看到的時(shí)候鸿市,我都雞凍了锯梁!

我們應(yīng)該客觀評價(jià) MVVM 和 ReactiveCocoa

但是但是,我突然想到焰情,我好象只需要一個(gè) ViewModel 而已陌凳,我完全可以簡單地做一個(gè) ViewModel 的工廠類或 Service 類就可以了,為什么要引入這么多框架内舟?現(xiàn)有的 MVC 真的有那么大的問題嗎合敦?

直到現(xiàn)在,ReactiveCocoa 在國內(nèi)外還都是在小眾領(lǐng)域验游,沒有被大量接受成為主流的編程框架充岛。不只是在 iOS 語言保檐,在別的語言中,例如 Java 中的 RxJava 也同樣沒有成為主流崔梗。

我在這里夜只,不是想說 ReactiveCocoa 不好,也不是想說 MVVM 不好蒜魄,而是想讓大家都能夠有一個(gè)客觀的認(rèn)識扔亥。ReactiveCocoa 和 MVVM 不應(yīng)該被神化,它是一種新穎的編程框架,能夠解決舊有編程框架的一些問題,但是也會帶來一些新問題礼华,僅此而已。如果不能使好的駕馭 ReactiveCocoa谦铃,同樣會造成 Controller 代碼過于復(fù)雜,代碼邏輯不易維護(hù)的問題榔昔。

總結(jié)

有一些人總是追趕著技術(shù)驹闰,有什么新技術(shù)不管三七二十一立馬就用,結(jié)果被各種坑撒会。

又有一些人嘹朗,總是擔(dān)心新技術(shù)帶來的技術(shù)風(fēng)險(xiǎn),不愿意學(xué)習(xí)诵肛。結(jié)果現(xiàn)在還有人在用 MRC 手動管理引用計(jì)數(shù)屹培。

而我想說,我們需要保持的是一個(gè)擁抱變化的心怔檩,以及理性分析的態(tài)度褪秀。在新技術(shù)的面前,不盲從薛训,也不守舊媒吗,一切的決策都應(yīng)該建立在認(rèn)真分析的基礎(chǔ)上,這樣才能應(yīng)對技術(shù)的變化乙埃。


文章轉(zhuǎn)自: http://www.infoq.com/cn/articles/rethinking-mvc-mvvm

作者 唐巧 發(fā)布于 2015年11月1日 |

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末闸英,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子介袜,更是在濱河造成了極大的恐慌甫何,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件遇伞,死亡現(xiàn)場離奇詭異辙喂,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進(jìn)店門巍耗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來秋麸,“玉大人,你說我怎么就攤上這事芍锦。” “怎么了飞盆?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵娄琉,是天一觀的道長。 經(jīng)常有香客問我吓歇,道長孽水,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任城看,我火速辦了婚禮女气,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘测柠。我一直安慰自己炼鞠,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布轰胁。 她就那樣靜靜地躺著谒主,像睡著了一般。 火紅的嫁衣襯著肌膚如雪赃阀。 梳的紋絲不亂的頭發(fā)上霎肯,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天,我揣著相機(jī)與錄音榛斯,去河邊找鬼观游。 笑死,一個(gè)胖子當(dāng)著我的面吹牛驮俗,可吹牛的內(nèi)容都是我干的懂缕。 我是一名探鬼主播,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼王凑,長吁一口氣:“原來是場噩夢啊……” “哼提佣!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起荤崇,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤拌屏,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后术荤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體倚喂,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了端圈。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片焦读。...
    茶點(diǎn)故事閱讀 38,643評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖舱权,靈堂內(nèi)的尸體忽然破棺而出矗晃,到底是詐尸還是另有隱情,我是刑警寧澤宴倍,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布张症,位于F島的核電站,受9級特大地震影響鸵贬,放射性物質(zhì)發(fā)生泄漏俗他。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一阔逼、第九天 我趴在偏房一處隱蔽的房頂上張望兆衅。 院中可真熱鬧,春花似錦嗜浮、人聲如沸羡亩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽夕春。三九已至,卻和暖如春专挪,著一層夾襖步出監(jiān)牢的瞬間及志,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工寨腔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留速侈,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓迫卢,卻偏偏與公主長得像倚搬,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子乾蛤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,509評論 2 348

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