響應式程式設計與MVVM架構—理論篇

VernonVan發表於2019-03-04
前段時間,在使用了一段時間的MVVM架構之後,我從實際的專案中抽離出來,對使用MVVM架構的整個過程進行了總結,對於架構、對於程式設計思維又有了不一樣的體會。於是提筆寫下自己探索MVVM架構的經驗和心得,以饗讀者。

本文會先對MVC架構做一個回顧,明確MVC中各層的職責;然後會提出MVVM架構的概念,本來接下來應該順勢舉幾個MVVM的例子進行說明的,但是考慮到響應式程式設計之於MVVM的重要性,所以在舉例之前會先講解一下響應式程式設計的概念(出於篇幅考慮,將MVVM架構實踐獨立成一篇文章,想直接看例項的請移駕這裡);最後會對MVC和MVVM的取捨談談自己的看法。話不多說,現在進入正題。

MVC架構

MVC(Model-View-Controller),是一種常見的客戶端軟體開發框架,具體到iOS上,絕大部分人從開始接觸iOS程式設計的時候都被告知MVC就是事實上的預設框架。系統也為我們實現好了公共的檢視類:UIView 和控制器類:UIViewController。大多數時候,我們都需要繼承這些類來實現我們的程式邏輯,因此,我們幾乎逃避不開MVC這種設計模式。下面就對MVC各層的職責進行明確:

Model層

Model層 是服務端資料在客戶端的對映,是薄薄的一層,完全可以用struct表徵。下面看一個例項:

響應式程式設計與MVVM架構—理論篇


可以看到,Model 層通常是服務端傳回的 JSON資料的對映,對應為一個一個的屬性。不過現在也有很多人將網路層(Service層)歸到Model中,也就是MVC(S)架構。同時,大部分時候資料的持久化操作也會放在Model層中。
總結一下,Model層的職責主要有以下幾項:HTTP請求、進行欄位驗證、持久化等。

View層

View層是展示在螢幕上的檢視的封裝,在 iOS 中也就是UIView以及UIView的子類。下面是UIView的繼承層級圖:

響應式程式設計與MVVM架構—理論篇


View層的職責是展示內容接受使用者的操作與事件

Controller層

看了Model層和View層如此簡單清晰的定義,如果你以為接下來要講的Controller層的定義也跟這兩層一樣,那你就要失望了。
粗略總結了一下,Controller層的職責包括但不限於:管理根檢視以及其子檢視的生命週期展示內容和佈局處理使用者行為(如按鈕的點選和手勢的觸發等)、儲存當前介面的狀態(例如分頁載入的頁數、是否正在進行網路請求的布林值等)、處理介面的跳轉作為UITableView以及其它容器檢視的代理以及資料來源業務邏輯各種動畫效果等。
畫風似乎不對啊,為什麼Controller層的職責比其他兩層加起來還多?

MVC的困境

因為MVC架構中Controller層往往程式碼很多,動輒2、3千行的這一特點,MVC也常常被調侃成是 Massive View Controller。造成這個問題的原因就是MVC的定義太過簡單樸素,要知道支撐一個尚不算大的企業級應用都動輒幾十萬行程式碼,還不包括各種依賴的第三方庫。這麼多的程式碼如何安置?按照傳統的MVC定義,分割了小部分到Model層和View層,剩下的程式碼都沒有其他地方可以去了,於是被統統的丟到了Controll層中。
龐大的Controller層帶來的問題就是難以維護、難以測試。而且其中充斥著大量的狀態值,一個任務的完成依賴於好幾個狀態值,而一個狀態值又同時參與到多個任務中,這樣複雜的多對多關係帶來的問題就是開發效率低下,需要花費大量的時間周旋在各個狀態值之間,對以後的功能擴充、業務新增也造成了障礙。
這樣的前提下,架構的改進就顯得非常有必要了。

MVVM架構初探

MVVM(Model-View-ViewModel),2005年由微軟的WPF和Silverlight的架構師 John Gossman 提出,是MVP模式與WPF結合發展演變過來的一種架構框架。MVVM實質上還是MVC架構範圍,是一個精心優化的MVC架構,所以與MVC架構是相容的。

MVVM首先將View層和Controller層進行了合併,統稱為View層,因為View層和Controller層往往是一起出現的。然後引入了一個新的模組 — ViewModel層,ViewModel層承載的內容就是之前在Controller層中檢視展現邏輯。MVVM的圖示如下:

響應式程式設計與MVVM架構—理論篇


什麼是檢視展現邏輯呢?在一款應用中,資料的來源可能是服務端返回、資料庫獲取和使用者輸入,然後儲存在Model中,但是這樣的資料是一種“未經格式化的”原始資料,還不能直接顯示到螢幕上。比如Model中可能有姓、名、暱稱等屬性,在某些介面中需要顯示成"姓名"的樣式,某些介面中顯示成"名姓"的樣式,某些介面中顯示"暱稱"的樣式。檢視展現邏輯就是把這些原始資料經過業務需求處理成展現到螢幕上的資料。可以把一個應用看成是播出一個新聞節目,Model層就是一大堆繁雜的稿件,View層就是主持人實際播報的新聞,而ViewModel層就是幕後的編輯處理團隊,負責從凌亂的稿件中抽出需要的資訊,整理成播報時用的稿件。這樣主持人拿著整理好的稿件,就能輕鬆的播報新聞了。

但是呢,平白無故多了一個ViewModel層。多一個層帶來的直接問題就是資訊的傳遞問題,層與層之間需要互通訊息,進行交流。在MVVM架構的實現中,開發人員想出了一個與傳統訊息傳遞所不一樣的方式,這就引出了響應式程式設計的概念。

響應式程式設計

響應式程式設計(Reactive Programming),是一種面向資料流和變化傳播的正規化。這意味著可以在程式語言中很方便地表達靜態或動態的資料流,而相關的計算模型會自動將變化的值通過資料流進行傳播。舉個維基百科中的例子:c:=a+b表示將表示式的結果賦給c,而之後改變a或b的值不會影響c。但在響應式程式設計中,c的值會隨著a或b的更新而更新。

響應式程式設計與MVVM架構—理論篇


也就是說,上圖中c的值最後會是5
同樣的例子還有Excel中的單元格,單元格可以包含字面值或類似"=B1+C1"的公式,而包含公式的單元格的值會依據其他單元格的值的變化而變化 。

如何實現所謂的響應式程式設計?在WPF中官方提供了Data Binding技術,macOS中也有類似的Cocoa Binding框架,但是在iOS中官方沒有提供這樣的框架。於是GitHub上出現了ReactiveCococa(以下簡稱為RAC)和RxSwift等優秀的第三方框架。
在RAC的思維中,iOS上的一切都是在變化的資料流,比如輸入框上使用者正在不斷輸入的文字、被點選的按鈕、旋轉縮放的檢視、不斷改變的NSString等等,這些就像是一個"水龍頭",當有變化產生的時候,水龍頭就會出水,把變化傳遞下去,對這個變化感興趣的人就可以在這個水龍頭上套一個"水管",這個人就成為了一個接收者(subscriber),當有變化產生的時候,接收者就能從水管中拿到這個變化的具體資訊。
RAC就提供了這樣的"水管",但是和現實中的水管有所不同,RAC有自己的一些限制:水管中傳遞的不是水,而是一個個的"玻璃球",這些玻璃球的直徑和水管的內徑一樣大,保證了玻璃球在水管中都是依次排列通過的,這就保證了不會出現多個玻璃球並列通過的情況。更加重要的是,在拿到玻璃球之前,可以對其進行一些個性化的定製。例如,可以在水龍頭上加一個過濾嘴(filter),不符合的不讓通過;也可以加一個改動裝置,把球改變成符合自己的需求(map);還可以把多個水龍頭合併成一個新的水龍頭(combineLatest:reduce:),經過了這些定製之後,出來的符合要求的玻璃球就能拿來直接用了。

在這麼強大的框架幫助下,MVVM所引入的ViewModel層和其他層之間的通訊問題得到了解決。

MVC還是MVVM?

在考慮是否要選擇MVVM架構之前,先來總結一下MVVM的優勢和不足。
MVVM主要有以下幾個優勢:
  • Controller層瘦身:將檢視展現邏輯抽出到ViewModel層帶來的直接變化就是Controller層變得更加輕量級,更加容易維護。
  • 更加容易測試:Controller層程式碼減少了,也意味著對Controller層的測試更加容易。
  • 相容MVC:選擇MVVM並不意味著完全摒棄MVC,MVVM相當於是MVC的超集,所以和MVC是相容的,也就是說,可以只在某一個模組中使用MVVM,不用擔心遷移架構時會造成需要全域性重構的問題。
  • 解決狀態以及狀態之間依賴過多的問題:這個優勢是由RAC所帶來的,響應式程式設計關注的資料的變化和流向,免除了一部分的狀態,而是直接將變化傳到顯示的控制元件上,例項在這裡。
  • 提供統一的訊息傳遞機制:這也是由RAC所帶來的,RAC對iOS程式設計中大部分的實物進行了抽象,提供了統一的介面,所以可以將iOS上KVO、通知(NSNotification)、委託(delegate)、Target-Action、塊(Block)等訊息傳遞方式統一,例項在這裡。
MVVM存在的問題主要有:
  • 學習曲線比較陡,通常需要引入第三方庫(ReactiveCocoa/RxSwift):使用MVVM通常需要引入第三方庫,而且需要轉換成響應式程式設計的思維方式,這是需要花費相當的學習適應時間的。
  • 建立更多的類:基本上每個Controller類會對應有一個ViewModel類
  • 效能上有一定影響,呼叫棧變深:RAC的實現底層依賴於KVO,帶來的問題是效能的損耗,比如光是subscribNext就慢了1個數量級,目前的回撥堆疊也比較深,最簡單的[signal subscribeNext^(id x){}]就會有近40次的呼叫。

MVVM好處不少,缺點也一堆。那到底要不要用MVVM呢?我覺得,在專案還不算臃腫的時候,可以簡單的對現有的MVC進行解耦優化,比如將網路層(Service層)、持久層(Storage層)等部分抽象出來即可。另一方面,MVVM對MVC也是相容的,可以考慮在專案中的某個模組試水MVVM,覺得好再逐步替換其他模組;而且很重要的一點是,響應式程式設計這樣一種正規化相當的鍛鍊我們的程式設計思維,讓我們可以站在資料的變化和流向的角度去思考我們的整一個專案,掌握這種思維方式也可以反哺到我們專案中別的地方。

架構沒有絕對的優劣,適合自己的架構就是最好的架構,那就讓我們理性分析,擁抱變化。



相關文章