iOS使用RAC實現MVVM的正經姿勢
從MVC到MVVM
前言
MVVM是微軟于2005年開發出的一種軟件架構設計模式,主要是為了在WPF和Silverlight中更簡單的對UI實現事件驅動編程。在WPF和Silverlight中,通過MVVM成功的實現了UI布局和數據邏輯的剝離。雖然WPF和Silverlight最后都沒有推廣開來,但是還是讓大家看到了MVVM設計模式的優秀之處。
我有幸在早年參加過Expression Blend的自動化測試工作,期間做了不少WPF和Silverlight的App,算是較早一批接觸熟悉MVVM的天朝碼農了。在iOS平臺出現了可以優雅實現MVVM的RAC時,著實激動了一下。下面就讓我們先從最早的MVC開始慢慢說起。
如果你想簡單點直接看代碼:Show you the code。
MVC理想設計模式
MVC是一種比較古老軟件架構設計模式,主旨是將代碼分為UI、數據和控制邏輯三大部分:
一個UI交互的整體過程:View接受用戶操作發送給Controller,Controller根據操作對數據進行修改,Controller接受數據修改的通知,并根據通知更新對應的UI。當然Controller可能有一些自有邏輯會修改數據或者更新UI,從屬關系上來說View和Model都屬于Controller。
MVC實例
這是我比較喜歡的一個實例,實現一個簡單的登錄界面。先羅列一下簡單的需求:
- 用戶名有效長度為4-16位,無效時對應文本框顯示為紅色底色,有效時文本框顯示為綠色底色,無輸入時顯示為白色底色。
- 密碼有效長度為8-16位,對應文本框底色邏輯與用戶名文本框一致。
- 登陸按鈕在用戶名和密碼均有效時可用,否則禁用。
為了讓代碼看起來不那么多,我使用xib來繪制了簡單的UI并完成了IBOutlet和delegate等的綁定。
然后呢需要寫的代碼就是大概下面這樣了:
這里的username
和password
兩個屬性可以看作Model層,文本框和按鈕的xib就是View層,VC主體代碼就是Controller層。可以看到所有的Model修改邏輯和UI更新邏輯都是在Controller里一起完成的。(完整代碼)
MVC解決的問題和優缺點
- 代碼成功分化為UI、數據和控制邏輯三大部分。
- 易于理解使用,普及成本低。
- Controller擁有View和Model,幾乎可以控制所有邏輯。
- 細節不夠明確,基本上不明確歸屬的代碼全部會放在Controller層。
- 和UI操作事件綁定較重,難以進行單元測試。
MVC實際使用狀況
因為上一節中提到的3和4兩點,很多代碼都只能寫在Controller層。還因為xib的特殊性,對多人協作十分不友好,導致大部分UI的布局和初始化代碼要用代碼實現,而這些代碼寫成單獨的類也多有不便,導致本該出現在View層的代碼也堆積在了Controller層。而且在iOS中,UIViewController和UIView本來就是一一對應的。這就導致了MVC從最早的Model-View-Controller最終一點點變成了Massive-View-Controller:
MVP設計模式
所謂設計模式,就是軟件設計過程中為了解決普遍性問題而提出的通用解決方案。MVP的出現就是為了解決MVC的Controller越來越臃腫的問題,進一步明確代碼的分工:
這個圖看上去和MVC很相似,但是這里的實虛線和MVC設計模式不同。所表示的意義為View層持有Presenter層,Presenter層持有Model層,View層并不可直接訪問到Model層。整體的UI交互流程和MVC類似。
這么做的意義就在于真正意義上的將UI邏輯和數據邏輯隔離,而隔離之后就可以更方便的對數據邏輯部分進行單元測試,隔離的另一個好處就是解開了一部分的耦合。
MVP實例
接著剛剛的實例,我們在它的基礎上繼續進行修改。
首先我們需要定義一個Presenter,頭文件內把所有可接受的用戶操作和更新UI需要用的回調定義好:
Presenter的內部實現:
可以看到Presenter做的事情就是把原來Controller的邏輯控制相關代碼抽離出來構建成一個單獨的類。接下來看一看對應的Controller現在變成什么樣:
現在Controller的代碼變得更加清晰了:兩個更新數據的調用,三個更新UI的調用,多了一些初始化Presenter的操作。
因為現在Presenter只包含邏輯,所以我們也較容易實現一個單元測試:
從結果可以看到Controller的代碼轉移了一部分到Presenter,MVP也成功把邏輯和UI代碼分離了。(完整代碼)
MVP優缺點
- UI布局和數據邏輯代碼劃分界限更明確。
- 理解難度尚可,較容易推廣。
- 解決了Controller的臃腫問題。
- Presenter-Model層可以進行單元測試。
- 需要額外寫大量接口定義和邏輯代碼(或者自己實現KVO監視)。
MVVM設計模式
隨著UI交互越來越復雜,MVP本身的一些缺點還是會暴露出來。
比如雖然是可以寫單元測試,但是單元測試寫起來還是有很多“啰嗦”的部分,需要模擬一些假的UI處理邏輯來進行結果的驗證,即使用block寫法這個部分的代碼量也省不了太多。
所有的用戶操作和更新UI的回調需要細細定義,隨著交互越來越復雜,這些定義都要有很大一坨代碼。
邏輯過于復雜的情況下,Present本身也會變得臃腫難以重用,代碼也會變的更加難以閱讀和維護。
這時候,MVVM出現了,為了解決以上大部分問題:
首先ViewModel-Model層和之前的Present-Model層一樣,沒有什么大的變化。View持有ViewModel,這個和MVP也一樣。變化主要在兩個方面:
- ViewModel相較于Present,不僅僅是個邏輯處理機,它附帶了自己的狀態,所以被才可以被稱為“Model”。ViewModel也因為這個變的更加獨立完整,我們更容易通過ViewModel的狀態去進行單元測試。Presenter在沒有設置回調的時候其實一直在做空運算而已,運算得到的值沒有進行存儲,下次必須重新運算。
- View不直接通過傳遞用戶操作來控制ViewModel,ViewModel也不直接通過回調來修改View。對常用的數據和UI控件的事件&屬性,MVVM框架的底層均進行了封裝,使得我們可以進行數據綁定操作。簡單來說我們可以用類似
[viewModel.username bind:usernameTextField.text]
類似的操作使得viewModel的屬性和UI控件的屬性相互綁定,其中一方修改的時候另一方直接自動做對應更改。這樣的話我們就不用重復的書寫很多回調操作,也不用處理一大堆UI控件的delegate事件。
其實MVVM的精華小部分在ViewModel,更大部分就在數據綁定,甚至有很多人覺得應該稱MVVM為MVB(Model-View-Binder)。
數據綁定引申出來的一個概念就是數據管道(轉換器),這個和大家學的數字電路比較相似:
這里我們有ABC三個數據源和兩個雙輸入的轉換器,我們可以進行組合得出各種想要的結果(如上圖),甚至于我們可以多次組合來完成更復雜的計算(如下圖):
這里的轉換器就帶來了第三點改進:
- 基于數據綁定和數據管道,可以對運算邏輯進行拆分和重用,最大程度的使代碼易讀易維護。
MVVM實例
還是接著剛剛的工程,首先要參照Reactive Cocoa的文檔把RAC添加到工程里。
ViewModel的定義
然后我們首先要把Present改造成ViewModel:
這里可以看到作為ViewModel輸出值的屬性設置成了readonly,剩下的username
和password
是輸入值。
單元測試
值得一提的是軟件工程中最好是測試驅動開發(TDD)而不是寫完邏輯再補測試,所以我們先改好單元測試:
從單元測試也很容易看出來ViewModel現在足夠獨立并易于測試。
View層和ViewModel層的綁定
我們再看一眼現在Controller應該怎么寫:
首先看到原來的一行loginButton
初始化代碼沒有了,因為數據綁定是自動更新的,初次綁定就會初始化狀態。
對ViewModel進行輸入數據的綁定,不再需要寫UITextFieldDelegate然后再傳遞事件,一行代碼完成綁定。
同樣將ViewModel的輸出數據綁定到UI,不需要再實現對應的回調,一樣一行代碼完成綁定。
這就是MVVM設計模式在最理想的情況下,Controller里需要和ViewModel交互的所有代碼內容。
數據管道(轉換器)
現在來說說剛剛的ConvertInputStateToColor
,它其實就是一個狀態到顏色的轉換器:
這里利用RACSignal的map方法做了一個映射,這就是我們的轉換器。當然我們以后也可以實現別的轉換器來進行方便的替換,比如實現一個僅在有效態顯示綠色其他狀態都顯示白色的轉換器。另外這個轉換器如果寫的更通用點,也可以被別的模塊重復使用。
ViewModel的UI無關性/轉換器組合的多樣可能性
這里要提一下為什么ViewModel不直接提供顏色值的輸出:
- ViewModel應該不關心具體的UI相關邏輯,只關心自己的邏輯正確和獨立完整性。
- 易于進行單元測試,枚舉當然比顏色值好檢查點……
- 提供更為基礎的狀態,這樣和不同的轉換器組合會產生更多的可能性。
這里的可能性指什么呢?舉個例子:出現了用戶有輸入內容時展示對應文本框清空按鈕的新需求。這時候我們只需要完成一個新的轉換器:InputStateEmpty
時返回isHidden = YES
;其余情況下返回isHidden = NO
。然后把對應輸出源通過轉換器綁定到清空按鈕的isHidden
屬性上即可。另外上一節提到的另一種顏色轉換器,也是一種多樣性的體現。
- 可以進行二次組合,用以計算輸出值
loginEnabled
。(見下一節)
ViewModel的完整實現
需要把輸出源對應的屬性偷偷改成readwrite的先,不然不可寫的話綁定的時候會跪。??
可以看到ViewModel現在就三塊邏輯:
- 內部實現了一個轉換器,監視
username
值更新對應的usernameInputState
值。 - 內部又實現了一個轉換器,監視
password
值更新對應的passwordInputState
值。 - 監視
usernameInputState
和passwordInputState
兩個輸出值,經過轉換再輸出loginEnabled
值。
這三塊邏輯都十分獨立且邏輯清晰,這就是MVVM或者說RAC帶來的優勢。
回想一下最早時候MVC里的Controller,在UITextField的回調里UI操作和數據邏輯混雜在一起,計算loginEnabled
屬性的邏輯還夾雜在計算文本框顏色的邏輯中。
相似的代碼可以再次合并
剛剛的代碼里,其實計算usernameInputState
和passwordInputState
兩個值的轉換器十分類似。如果以后還可能有類似的轉換需求,我們應該把它倆的轉換器再合并成獨立的轉換器,方便重用:
記得做好斷言防止寫錯調用代碼,不過看上去轉換器邏輯不需要額外做錯誤保護。
有了新的轉換器,如果以后出現了驗證碼限制長度為5之類的需求,它就有用武之地了。
在此基礎下ViewModel的代碼也再次簡化為:
可以看到代碼更清晰易懂了??,雖然貌似代碼量沒有減少多少???。
另外這里也看出來很靈活的一點,轉換器可以直接寫ViewModel里,也可以抽離成單獨的類,這需要根據具體情況來定不同的寫法。
為轉換器寫單元測試
簡單點的辦法是把邏輯從RACSignal的map方法里抽出來,這樣就可以單獨測試邏輯了:
添加完單元測試的完整MVVM設計模式實例代碼在這里:完整代碼。
當然,如果不想破壞轉換器類的實現方式,有另一種單元測試的方案(這個我會另寫一篇博客來介紹):
MVVM優缺點
- UI布局和數據邏輯代碼劃分界限更明確,數據邏輯還可以細分成各種轉換器。
- 很難理解正確使用姿勢,使用難度高容易出錯,且出錯調試難度也很大。
- 代碼量相較MVP應該有所減少,邏輯更清晰使得代碼易讀性重用性有所提高(用對姿勢的話)。
- 更方便實現單元測試。
- 內存和CPU開銷較大。
總結
設計模式不是銀彈,任何設計模式均有適用的場景,并沒有某種設計模式可以解決所有的問題。
比如UI交互較少較輕的頁面,用MVC直接實現就會很輕松。
比如團隊整體水平較低,強行使用MVVM也會面臨困境。
學習和了解新的設計模式主要是開拓自己的眼界,以后面臨問題的時候可以多一個新的選擇。
而且誰說MVC就不能用RAC做數據綁定呢?MVC的Controller太臃腫了,也可以用Category來分散代碼不是么?
來自http://blog.harrisonxi.com/2017/07/iOS使用RAC實現MVVM的正經姿勢.html