iOS MVC、MVP、MVVM的正確使用姿勢(shì)

iOS使用RAC實(shí)現(xiàn)MVVM的正經(jīng)姿勢(shì)

從MVC到MVVM

前言

MVVM是微軟于2005年開發(fā)出的一種軟件架構(gòu)設(shè)計(jì)模式,主要是為了在WPF和Silverlight中更簡(jiǎn)單的對(duì)UI實(shí)現(xiàn)事件驅(qū)動(dòng)編程。在WPF和Silverlight中,通過MVVM成功的實(shí)現(xiàn)了UI布局和數(shù)據(jù)邏輯的剝離。雖然WPF和Silverlight最后都沒有推廣開來,但是還是讓大家看到了MVVM設(shè)計(jì)模式的優(yōu)秀之處。

我有幸在早年參加過Expression Blend的自動(dòng)化測(cè)試工作,期間做了不少WPF和Silverlight的App,算是較早一批接觸熟悉MVVM的天朝碼農(nóng)了。在iOS平臺(tái)出現(xiàn)了可以優(yōu)雅實(shí)現(xiàn)MVVM的RAC時(shí),著實(shí)激動(dòng)了一下。下面就讓我們先從最早的MVC開始慢慢說起。

如果你想簡(jiǎn)單點(diǎn)直接看代碼:Show you the code。

MVC理想設(shè)計(jì)模式

MVC是一種比較古老軟件架構(gòu)設(shè)計(jì)模式,主旨是將代碼分為UI、數(shù)據(jù)和控制邏輯三大部分:

18-A

一個(gè)UI交互的整體過程:View接受用戶操作發(fā)送給Controller,Controller根據(jù)操作對(duì)數(shù)據(jù)進(jìn)行修改,Controller接受數(shù)據(jù)修改的通知,并根據(jù)通知更新對(duì)應(yīng)的UI。當(dāng)然Controller可能有一些自有邏輯會(huì)修改數(shù)據(jù)或者更新UI,從屬關(guān)系上來說View和Model都屬于Controller。

MVC實(shí)例

這是我比較喜歡的一個(gè)實(shí)例,實(shí)現(xiàn)一個(gè)簡(jiǎn)單的登錄界面。先羅列一下簡(jiǎn)單的需求:

  1. 用戶名有效長(zhǎng)度為4-16位,無效時(shí)對(duì)應(yīng)文本框顯示為紅色底色,有效時(shí)文本框顯示為綠色底色,無輸入時(shí)顯示為白色底色。
  2. 密碼有效長(zhǎng)度為8-16位,對(duì)應(yīng)文本框底色邏輯與用戶名文本框一致。
  3. 登陸按鈕在用戶名和密碼均有效時(shí)可用,否則禁用。
18-B

為了讓代碼看起來不那么多,我使用xib來繪制了簡(jiǎn)單的UI并完成了IBOutlet和delegate等的綁定。

然后呢需要寫的代碼就是大概下面這樣了:

18-C

這里的usernamepassword兩個(gè)屬性可以看作Model層,文本框和按鈕的xib就是View層,VC主體代碼就是Controller層??梢钥吹剿械腗odel修改邏輯和UI更新邏輯都是在Controller里一起完成的。(完整代碼

MVC解決的問題和優(yōu)缺點(diǎn)

  • 代碼成功分化為UI、數(shù)據(jù)和控制邏輯三大部分。
  • 易于理解使用,普及成本低。
  • Controller擁有View和Model,幾乎可以控制所有邏輯。
  • 細(xì)節(jié)不夠明確,基本上不明確歸屬的代碼全部會(huì)放在Controller層。
  • 和UI操作事件綁定較重,難以進(jìn)行單元測(cè)試。

MVC實(shí)際使用狀況

因?yàn)樯弦还?jié)中提到的3和4兩點(diǎn),很多代碼都只能寫在Controller層。還因?yàn)閤ib的特殊性,對(duì)多人協(xié)作十分不友好,導(dǎo)致大部分UI的布局和初始化代碼要用代碼實(shí)現(xiàn),而這些代碼寫成單獨(dú)的類也多有不便,導(dǎo)致本該出現(xiàn)在View層的代碼也堆積在了Controller層。而且在iOS中,UIViewController和UIView本來就是一一對(duì)應(yīng)的。這就導(dǎo)致了MVC從最早的Model-View-Controller最終一點(diǎn)點(diǎn)變成了Massive-View-Controller

18-D

MVP設(shè)計(jì)模式

所謂設(shè)計(jì)模式,就是軟件設(shè)計(jì)過程中為了解決普遍性問題而提出的通用解決方案。MVP的出現(xiàn)就是為了解決MVC的Controller越來越臃腫的問題,進(jìn)一步明確代碼的分工:

18-E

這個(gè)圖看上去和MVC很相似,但是這里的實(shí)虛線和MVC設(shè)計(jì)模式不同。所表示的意義為View層持有Presenter層,Presenter層持有Model層,View層并不可直接訪問到Model層。整體的UI交互流程和MVC類似。

這么做的意義就在于真正意義上的將UI邏輯和數(shù)據(jù)邏輯隔離,而隔離之后就可以更方便的對(duì)數(shù)據(jù)邏輯部分進(jìn)行單元測(cè)試,隔離的另一個(gè)好處就是解開了一部分的耦合。

MVP實(shí)例

接著剛剛的實(shí)例,我們?cè)谒幕A(chǔ)上繼續(xù)進(jìn)行修改。

首先我們需要定義一個(gè)Presenter,頭文件內(nèi)把所有可接受的用戶操作和更新UI需要用的回調(diào)定義好:

18-F

Presenter的內(nèi)部實(shí)現(xiàn):

18-G

可以看到Presenter做的事情就是把原來Controller的邏輯控制相關(guān)代碼抽離出來構(gòu)建成一個(gè)單獨(dú)的類。接下來看一看對(duì)應(yīng)的Controller現(xiàn)在變成什么樣:

18-H

現(xiàn)在Controller的代碼變得更加清晰了:兩個(gè)更新數(shù)據(jù)的調(diào)用,三個(gè)更新UI的調(diào)用,多了一些初始化Presenter的操作。

因?yàn)楝F(xiàn)在Presenter只包含邏輯,所以我們也較容易實(shí)現(xiàn)一個(gè)單元測(cè)試:

18-I

從結(jié)果可以看到Controller的代碼轉(zhuǎn)移了一部分到Presenter,MVP也成功把邏輯和UI代碼分離了。(完整代碼

MVP優(yōu)缺點(diǎn)

  • UI布局和數(shù)據(jù)邏輯代碼劃分界限更明確。
  • 理解難度尚可,較容易推廣。
  • 解決了Controller的臃腫問題。
  • Presenter-Model層可以進(jìn)行單元測(cè)試。
  • 需要額外寫大量接口定義和邏輯代碼(或者自己實(shí)現(xiàn)KVO監(jiān)視)。

MVVM設(shè)計(jì)模式

隨著UI交互越來越復(fù)雜,MVP本身的一些缺點(diǎn)還是會(huì)暴露出來。

比如雖然是可以寫單元測(cè)試,但是單元測(cè)試寫起來還是有很多“啰嗦”的部分,需要模擬一些假的UI處理邏輯來進(jìn)行結(jié)果的驗(yàn)證,即使用block寫法這個(gè)部分的代碼量也省不了太多。

所有的用戶操作和更新UI的回調(diào)需要細(xì)細(xì)定義,隨著交互越來越復(fù)雜,這些定義都要有很大一坨代碼。

邏輯過于復(fù)雜的情況下,Present本身也會(huì)變得臃腫難以重用,代碼也會(huì)變的更加難以閱讀和維護(hù)。

這時(shí)候,MVVM出現(xiàn)了,為了解決以上大部分問題:

18-J

首先ViewModel-Model層和之前的Present-Model層一樣,沒有什么大的變化。View持有ViewModel,這個(gè)和MVP也一樣。變化主要在兩個(gè)方面:

  1. ViewModel相較于Present,不僅僅是個(gè)邏輯處理機(jī),它附帶了自己的狀態(tài),所以被才可以被稱為“Model”。ViewModel也因?yàn)檫@個(gè)變的更加獨(dú)立完整,我們更容易通過ViewModel的狀態(tài)去進(jìn)行單元測(cè)試。Presenter在沒有設(shè)置回調(diào)的時(shí)候其實(shí)一直在做空運(yùn)算而已,運(yùn)算得到的值沒有進(jìn)行存儲(chǔ),下次必須重新運(yùn)算。
  2. View不直接通過傳遞用戶操作來控制ViewModel,ViewModel也不直接通過回調(diào)來修改View。對(duì)常用的數(shù)據(jù)和UI控件的事件&屬性,MVVM框架的底層均進(jìn)行了封裝,使得我們可以進(jìn)行數(shù)據(jù)綁定操作。簡(jiǎn)單來說我們可以用類似[viewModel.username bind:usernameTextField.text]類似的操作使得viewModel的屬性和UI控件的屬性相互綁定,其中一方修改的時(shí)候另一方直接自動(dòng)做對(duì)應(yīng)更改。這樣的話我們就不用重復(fù)的書寫很多回調(diào)操作,也不用處理一大堆UI控件的delegate事件。

其實(shí)MVVM的精華小部分在ViewModel,更大部分就在數(shù)據(jù)綁定,甚至有很多人覺得應(yīng)該稱MVVM為MVB(Model-View-Binder)。

數(shù)據(jù)綁定引申出來的一個(gè)概念就是數(shù)據(jù)管道(轉(zhuǎn)換器),這個(gè)和大家學(xué)的數(shù)字電路比較相似:

18-K

這里我們有ABC三個(gè)數(shù)據(jù)源和兩個(gè)雙輸入的轉(zhuǎn)換器,我們可以進(jìn)行組合得出各種想要的結(jié)果(如上圖),甚至于我們可以多次組合來完成更復(fù)雜的計(jì)算(如下圖):

18-L

這里的轉(zhuǎn)換器就帶來了第三點(diǎn)改進(jìn):

  1. 基于數(shù)據(jù)綁定和數(shù)據(jù)管道,可以對(duì)運(yùn)算邏輯進(jìn)行拆分和重用,最大程度的使代碼易讀易維護(hù)。

MVVM實(shí)例

還是接著剛剛的工程,首先要參照Reactive Cocoa的文檔把RAC添加到工程里。

ViewModel的定義

然后我們首先要把Present改造成ViewModel:

19-A

這里可以看到作為ViewModel輸出值的屬性設(shè)置成了readonly,剩下的usernamepassword是輸入值。

單元測(cè)試

值得一提的是軟件工程中最好是測(cè)試驅(qū)動(dòng)開發(fā)(TDD)而不是寫完邏輯再補(bǔ)測(cè)試,所以我們先改好單元測(cè)試:

19-B

從單元測(cè)試也很容易看出來ViewModel現(xiàn)在足夠獨(dú)立并易于測(cè)試。

View層和ViewModel層的綁定

我們?cè)倏匆谎郜F(xiàn)在Controller應(yīng)該怎么寫:

19-C

首先看到原來的一行loginButton初始化代碼沒有了,因?yàn)閿?shù)據(jù)綁定是自動(dòng)更新的,初次綁定就會(huì)初始化狀態(tài)。

對(duì)ViewModel進(jìn)行輸入數(shù)據(jù)的綁定,不再需要寫UITextFieldDelegate然后再傳遞事件,一行代碼完成綁定。

同樣將ViewModel的輸出數(shù)據(jù)綁定到UI,不需要再實(shí)現(xiàn)對(duì)應(yīng)的回調(diào),一樣一行代碼完成綁定。

這就是MVVM設(shè)計(jì)模式在最理想的情況下,Controller里需要和ViewModel交互的所有代碼內(nèi)容。

數(shù)據(jù)管道(轉(zhuǎn)換器)

現(xiàn)在來說說剛剛的ConvertInputStateToColor,它其實(shí)就是一個(gè)狀態(tài)到顏色的轉(zhuǎn)換器:

19-D
19-E

這里利用RACSignal的map方法做了一個(gè)映射,這就是我們的轉(zhuǎn)換器。當(dāng)然我們以后也可以實(shí)現(xiàn)別的轉(zhuǎn)換器來進(jìn)行方便的替換,比如實(shí)現(xiàn)一個(gè)僅在有效態(tài)顯示綠色其他狀態(tài)都顯示白色的轉(zhuǎn)換器。另外這個(gè)轉(zhuǎn)換器如果寫的更通用點(diǎn),也可以被別的模塊重復(fù)使用。

ViewModel的UI無關(guān)性/轉(zhuǎn)換器組合的多樣可能性

這里要提一下為什么ViewModel不直接提供顏色值的輸出:

  1. ViewModel應(yīng)該不關(guān)心具體的UI相關(guān)邏輯,只關(guān)心自己的邏輯正確和獨(dú)立完整性。
  2. 易于進(jìn)行單元測(cè)試,枚舉當(dāng)然比顏色值好檢查點(diǎn)……
  3. 提供更為基礎(chǔ)的狀態(tài),這樣和不同的轉(zhuǎn)換器組合會(huì)產(chǎn)生更多的可能性。

這里的可能性指什么呢?舉個(gè)例子:出現(xiàn)了用戶有輸入內(nèi)容時(shí)展示對(duì)應(yīng)文本框清空按鈕的新需求。這時(shí)候我們只需要完成一個(gè)新的轉(zhuǎn)換器:InputStateEmpty時(shí)返回isHidden = YES;其余情況下返回isHidden = NO。然后把對(duì)應(yīng)輸出源通過轉(zhuǎn)換器綁定到清空按鈕的isHidden屬性上即可。另外上一節(jié)提到的另一種顏色轉(zhuǎn)換器,也是一種多樣性的體現(xiàn)。

  1. 可以進(jìn)行二次組合,用以計(jì)算輸出值loginEnabled。(見下一節(jié))

ViewModel的完整實(shí)現(xiàn)

19-F

需要把輸出源對(duì)應(yīng)的屬性偷偷改成readwrite的先,不然不可寫的話綁定的時(shí)候會(huì)跪。??

可以看到ViewModel現(xiàn)在就三塊邏輯:

  1. 內(nèi)部實(shí)現(xiàn)了一個(gè)轉(zhuǎn)換器,監(jiān)視username值更新對(duì)應(yīng)的usernameInputState值。
  2. 內(nèi)部又實(shí)現(xiàn)了一個(gè)轉(zhuǎn)換器,監(jiān)視password值更新對(duì)應(yīng)的passwordInputState值。
  3. 監(jiān)視usernameInputStatepasswordInputState兩個(gè)輸出值,經(jīng)過轉(zhuǎn)換再輸出loginEnabled值。

這三塊邏輯都十分獨(dú)立且邏輯清晰,這就是MVVM或者說RAC帶來的優(yōu)勢(shì)。

回想一下最早時(shí)候MVC里的Controller,在UITextField的回調(diào)里UI操作和數(shù)據(jù)邏輯混雜在一起,計(jì)算loginEnabled屬性的邏輯還夾雜在計(jì)算文本框顏色的邏輯中。

相似的代碼可以再次合并

剛剛的代碼里,其實(shí)計(jì)算usernameInputStatepasswordInputState兩個(gè)值的轉(zhuǎn)換器十分類似。如果以后還可能有類似的轉(zhuǎn)換需求,我們應(yīng)該把它倆的轉(zhuǎn)換器再合并成獨(dú)立的轉(zhuǎn)換器,方便重用:

19-G
19-H

記得做好斷言防止寫錯(cuò)調(diào)用代碼,不過看上去轉(zhuǎn)換器邏輯不需要額外做錯(cuò)誤保護(hù)。

有了新的轉(zhuǎn)換器,如果以后出現(xiàn)了驗(yàn)證碼限制長(zhǎng)度為5之類的需求,它就有用武之地了。

在此基礎(chǔ)下ViewModel的代碼也再次簡(jiǎn)化為:

19-I

可以看到代碼更清晰易懂了??,雖然貌似代碼量沒有減少多少???。

另外這里也看出來很靈活的一點(diǎn),轉(zhuǎn)換器可以直接寫ViewModel里,也可以抽離成單獨(dú)的類,這需要根據(jù)具體情況來定不同的寫法。

為轉(zhuǎn)換器寫單元測(cè)試

簡(jiǎn)單點(diǎn)的辦法是把邏輯從RACSignal的map方法里抽出來,這樣就可以單獨(dú)測(cè)試邏輯了:

19-J

添加完單元測(cè)試的完整MVVM設(shè)計(jì)模式實(shí)例代碼在這里:完整代碼

當(dāng)然,如果不想破壞轉(zhuǎn)換器類的實(shí)現(xiàn)方式,有另一種單元測(cè)試的方案(這個(gè)我會(huì)另寫一篇博客來介紹):

19-K

MVVM優(yōu)缺點(diǎn)

  1. UI布局和數(shù)據(jù)邏輯代碼劃分界限更明確,數(shù)據(jù)邏輯還可以細(xì)分成各種轉(zhuǎn)換器。
  2. 很難理解正確使用姿勢(shì),使用難度高容易出錯(cuò),且出錯(cuò)調(diào)試難度也很大。
  3. 代碼量相較MVP應(yīng)該有所減少,邏輯更清晰使得代碼易讀性重用性有所提高(用對(duì)姿勢(shì)的話)。
  4. 更方便實(shí)現(xiàn)單元測(cè)試。
  5. 內(nèi)存和CPU開銷較大。

總結(jié)

設(shè)計(jì)模式不是銀彈,任何設(shè)計(jì)模式均有適用的場(chǎng)景,并沒有某種設(shè)計(jì)模式可以解決所有的問題。

比如UI交互較少較輕的頁面,用MVC直接實(shí)現(xiàn)就會(huì)很輕松。

比如團(tuán)隊(duì)整體水平較低,強(qiáng)行使用MVVM也會(huì)面臨困境。

學(xué)習(xí)和了解新的設(shè)計(jì)模式主要是開拓自己的眼界,以后面臨問題的時(shí)候可以多一個(gè)新的選擇。

而且誰說MVC就不能用RAC做數(shù)據(jù)綁定呢?MVC的Controller太臃腫了,也可以用Category來分散代碼不是么?

來自http://blog.harrisonxi.com/2017/07/iOS使用RAC實(shí)現(xiàn)MVVM的正經(jīng)姿勢(shì).html

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

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

  • 前言,在游戲開發(fā)中,經(jīng)常會(huì)聽到MVC、MVP、MVVM 這類名詞,對(duì)他們的第一印象多是為了解耦、提高擴(kuò)展性而選擇的...
    su9257_海瀾閱讀 2,241評(píng)論 0 1
  • 簡(jiǎn)書博客已經(jīng)暫停更新,想看更多技術(shù)博客請(qǐng)到: 掘金 :J_Knight_ 個(gè)人博客: J_Knight_ 個(gè)人公眾...
    J_Knight_閱讀 10,222評(píng)論 80 187
  • 用到的組件 1、通過CocoaPods安裝 2、第三方類庫安裝 3、第三方服務(wù) 友盟社會(huì)化分享組件 友盟用戶反饋 ...
    SunnyLeong閱讀 15,205評(píng)論 1 180
  • 漸變的面目拼圖要我怎么拼? 我是疲乏了還是投降了? 不是不允許自己墜落, 我沒有滴水不進(jìn)的保護(hù)膜。 就是害怕變得面...
    悶熱當(dāng)乘涼閱讀 4,502評(píng)論 0 13
  • 感覺自己有點(diǎn)神經(jīng)衰弱,總是覺得手機(jī)響了;屋外有人走過;每次媽媽不聲不響的進(jìn)房間突然跟我說話,我都會(huì)被嚇得半死!一整...
    章魚的擁抱閱讀 2,414評(píng)論 4 5

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