iOS VIPER架構實踐(一):從MVC到MVVM到VIPER

最近半年在寫app的時候,研究了一下各種iOS代碼架構,最后選擇了VIPER進行實踐,在此對實踐中遇到的各種設計問題做一番總結,并分享造出的輪子。

對代碼風格和架構有興趣的同學,肯定都已經在很多地方見過各種架構的介紹。MVC、MVP、MVVM、VIPER,細分程度逐漸上升。這些架構設計大部分都是來自MVC,只是各自用不同的方式對MVC進行了細分,在此只對MVC、MVP和MVVM作精簡介紹,想要詳細了解可以參考這些文章:

iOS 架構模式–解密 MVC,MVP,MVVM以及VIPER架構,

淺談 MVC、MVP 和 MVVM 架構模式

MVC

Model-View-Controller。MVC簡單地將一個模塊分為3部分:

  • View是展示給外部的界面
  • Model是Controller內部管理的數據模型,和各種數據操作工具
  • Controller負責將Model的變化更新到View
  • Controller負責處理來自View的事件

MVC的劃分粒度很粗,因此有很多種具體實現,各個實現有差異,因此并沒有一個十分明確的標準定義。

蘋果的MVC

蘋果的Cocoa Touch就遵照了MVC的設計,一個界面分為UIView和UIViewController,UIView負責渲染和接收觸摸事件,UIViewController負責子view之間的布局、組合、更新以及事件處理。

盡管蘋果已經給我們提供了簡單的MVC支持,但是在實踐中我們卻常常沒有遵守MVC。原因在于Cocoa Touch中的Model部分是由我們自己負責管理的,并沒有提供原生的設計支持。所以有時候會出現這樣的情況:一個UIView為了方便,提供了一個從某個model進行配置的方法。乍一看十分合理,但是仔細想想就會發現,這么做已經將View和Model耦合,不符合蘋果官方的MVC規范(The Role of View Controllers)。

另外,UIViewController存在的一些問題,導致了它很容易變得臃腫和耦合。

首先,UIViewController和UIView耦合得十分緊密,導致UIViewController經常和某些具體的UIView耦合,幾乎無法重用。而且在測試的時候,很難做到單獨測試沒有View的那部分代碼,因為在寫的時候就很容易將View的邏輯入侵到各處,Controller會受到View的狀態的影響,無法穩定測試。因此,應該盡量把和View無關的代碼放到UIViewController之外。

第二,UIViewController負責了界面跳轉的操作,界面跳轉的相關配置是直接在對應的UIViewController實例上設置的,這樣就很容易把源界面和目的界面耦合起來,簡單地把界面跳轉的部分單獨抽離為一個封裝好的跳轉方法可以一定程度上減少這部分耦合,但也不可避免地會多寫許多代碼。

因此,蘋果的MVC,實際上是Model-View-ViewController。它是一個視圖驅動的設計,Controller只是為了管理View而存在的。蘋果把UIViewController和Model的關系設計交給了我們自己。所以,如何把一個UIViewController進行更明確的分工,就是這些架構要做的事。

MVP

Model-View-Presenter用一個Presenter,把Controller中View的部分剔除,實現了View和Model的隔絕。各部分分工如下:

  • View負責界面展示和布局管理,向Presenter暴露視圖更新和數據獲取的接口
  • Presenter負責接收來自View的事件,通過View提供的接口更新視圖,并管理Model
  • Model和MVC中的一樣,提供數據模型和數據操作

在iOS里,UIView和UIViewController共同組合成了MVP中的View。UIView負責元素的展示,UIViewController負責界面布局和組合,并把事件轉發給Presenter。
因此在MVP里,業務邏輯被放到了Presenter中,由它負責協調View和Model。而由于View的抽離,Presenter的狀態是可控的,在測試時更不容易受外部影響。

在iOS中使用MVP很簡單,在View和Presenter之間用protocol做好事件傳遞就可以。缺點就是多了一層用于隔離的接口,會導致代碼數量增大。

但是隨著界面越來越復雜,Presenter中的業務代碼也會越來越龐大,總有一天會遇到一個新的問題:如何再細分Presenter。

MVVM

Model-View-ViewModel模式,它也和MVP一樣,目的是解決View和Model的耦合。各部分分工如下:

最普遍的MVVM

  • Model提供數據模型
  • View負責視圖展示
  • ViewModel用于描述View的狀態,例如View的顏色、顯示的文字等屬性類的信息,將View抽象成了一個特殊的模型,并且持有和管理Model,維護業務邏輯

在MVP中,View通過接口的方式來描述自己,在MVVM中,則通過ViewModel來描述自己的特征。那么ViewModel如何將自己的變化更新到View上呢?MVVM經常和數據綁定一起出現,在UIViewController中,將View和ViewModel的屬性用類似KVO的方式進行綁定,這樣ViewModel的變化就能立即傳輸到View上。

數據綁定

利用ReactiveCocoa和RxSwift這些函數式響應編程框架實現數據綁定,可以用很少的代碼完成復雜的業務邏輯,熟練時能夠提升開發速度。但是數據綁定的缺點也很明顯:調試困難,數據來源難以回溯,在線上出bug的時候就很難追蹤了,所以從這方面來說又降低了維護的效率。

其實數據綁定只是一種為了減少膠水代碼的技術實現方式,MVVM的設計并沒有要求必須要使用數據綁定,你也完全可以使用protocol的方式來將ViewModel的變化傳遞給View,讓數據流向更清晰。MVVM的關鍵是將View進行了抽象,從而實現View和Model的解耦。

ViewModel的職責

但是除了數據綁定,MVVM還有另一個問題。把業務邏輯放到ViewModel中,雖然能夠為UIViewController減負,但是只是把問題轉移了,最終ViewModel還是會變成另一個Massive ViewModel。

而且當ViewModel維護Model和業務邏輯時,可復用性就會大大降低。例如把同一個登錄界面復用到另一個app中時,login model中的屬性名或者類型很可能會改變,從而數據處理的方式也會改變,導致ViewModel無法重用。而當View由多個子View組成時,ViewModel里也會引入多個子ViewModel,這就又導致了View的實現影響了ViewModel的實現。奇怪的是,國內iOS圈對這個問題的探討十分稀少。

ViewModel到底是什么?從它的命名和最初的設計來看,它只是View的抽象,目的是方便和Model進行數據轉換。而默認把業務邏輯也放到ViewModel里,大概是由于objc.io上那篇文章的影響。其實在微軟的WPF和前端里,MVVM的業務邏輯大部分是放在Model層的,相關的討論可以參考:

MVVM: ViewModel and Business Logic Connection

Where does business logic sit in MVVM?

The Problems with MVVM on iOS

而針對這個問題,有人又提出了一個MVVMP架構(Model-View-ViewModel-Presenter),把業務邏輯放到了Presenter里。Presenter的引入讓ViewModel專注于View的抽象,和Model分離開來,只負責管理View相關的狀態、傳遞View的事件,因此ViewModel中的代碼可以得到很好的復用。而Presenter負責大部分業務邏輯,如果模塊需要重用,則把業務邏輯中的數據操作邏輯(domain logic)單獨分離出來作為重用代碼,其他的無法重用的應用邏輯(application logic)則依舊放在Presenter里。

和MVP相比,MVVM用了一種更優雅的方式來抽象View。但它和MVP其實是類似的,只做了View和Model的解耦,仍然沒有對Controller進行進一步的細分。

那么如何對Controller進行進一步的職責細分呢?答案就是VIPER。

VIPER

VIPER的全稱是View-Interactor-Presenter-Entity-Router。示意圖如下:

VIPER

相比之前的MVX架構,VIPER多出了兩個東西:Interactor(交互器)和Router(路由)。

各部分職責如下:

View

  • 提供完整的視圖,負責視圖的組合、布局、更新
  • 向Presenter提供更新視圖的接口
  • 將View相關的事件發送給Presenter

Presenter

  • 接收并處理來自View的事件
  • 向Interactor請求調用業務邏輯
  • 向Interactor提供View中的數據
  • 接收并處理來自Interactor的數據回調事件
  • 通知View進行更新操作
  • 通過Router跳轉到其他View

Router

  • 提供View之間的跳轉功能,減少了模塊間的耦合
  • 初始化VIPER的各個模塊

Interactor

  • 維護主要的業務邏輯功能,向Presenter提供現有的業務用例
  • 維護、獲取、更新Entity
  • 當有業務相關的事件發生時,處理事件,并通知Presenter

Entity

  • 和Model一樣的數據模型

和MVX的區別

VIPER把MVC中的Controller進一步拆分成了Presenter、Router和Interactor。和MVP中負責業務邏輯的Presenter不同,VIPER的Presenter的主要工作是在View和Interactor之間傳遞事件,并管理一些View的展示邏輯,主要的業務邏輯實現代碼都放在了Interactor里。Interactor的設計里提出了"用例"的概念,也就是把每一個會出現的業務流程封裝好,這樣可測試性會大大提高。而Router則進一步解決了不同模塊之間的耦合。所以,VIPER和上面幾個MVX相比,多總結出了幾個需要維護的東西:

  • View事件管理
  • 數據事件管理
  • 事件和業務的轉化
  • 總結每個業務用例
  • 模塊內分層隔離
  • 模塊間通信

而這里面,還可以進一步細分一些職責。VIPER實際上已經把Controller的概念淡化了,這拆分出來的幾個部分,都有很明確的單一職責,有些部分之間是完全隔絕的,在開發時就應該清晰地區分它們各自的職責,而不是將它們視為一個Controller。

優點

VIPER的特色就是職責明確,粒度細,隔離關系明確,這樣能帶來很多優點:

  • 可測試性好。UI測試和業務邏輯測試可以各自單獨進行。
  • 易于迭代。各部分遵循單一職責,可以很明確地知道新的代碼應該放在哪里。
  • 隔離程度高,耦合程度低。一個模塊的代碼不容易影響到另一個模塊。
  • 易于團隊合作。各部分分工明確,團隊合作時易于統一代碼風格,可以快速接手別人的代碼。

缺點

  • 一個模塊內的類數量增大,代碼量增大,在層與層之間需要花更多時間設計接口。

使用代碼模板來自動生成文件和模板代碼可以減少很多重復勞動,而花費時間設計和編寫接口是減少耦合的路上不可避免的,你也可以使用數據綁定這樣的技術來減少一些傳遞的層次。

  • 模塊的初始化較為復雜,打開一個新的界面需要生成View、Presenter、Interactor,并且設置互相之間的依賴關系。而iOS中缺少這種設置復雜初始化的原生方式。

簡單來說,就是Cocoa框架缺少一個強大的自定義依賴注入工具。這個問題影響不是特別大,可以選用一些第三方工具來實現,也可以在Router的界面跳轉方法里,對模塊進行初始化,只不過總是不夠完美。針對這個問題,我實現了一個基于protocol聲明依賴的界面跳轉Router,將會在之后的文章中進行詳解。

總結

有人可能會覺得,一個界面模塊真的有必要使用這么復雜的架構嗎?這樣是不是過度設計?

我反對這種觀點。不要被VIPER的組織圖嚇到,VIPER并不復雜,它是將原來MVC中的Controller中的各種任務進行了清晰的分解,在寫代碼時,你會很清楚你正在做什么。事實上,它比使用了數據綁定技術的MVVM更加簡單,就是因為它職責明確。從MVC轉到VIPER的過程同樣是很清晰的,它甚至把重構的思路都體現出來了。而MVVM則留下了許多尚未明確的責任,導致不同的人會在某些地方有不同的實現。即便你還在使用MVC,你也應該在Controller中分離出VIPER總結出的那些專項職責,既然如此,為何不徹底地明確這些職責,把它們分散到不同的文件中呢?一旦開始這樣的工作,你就已經向VIPER靠攏了。

有人可能會覺得,VIPER適合大型app,中小型app沒必要過早使用。

我反對這種觀點。VIPER是單個界面模塊內的架構設計,并不是整個app架構層面的設計,和app的整體架構沒有多大的關系,也不存在過早使用VIPER的情況。所以,嚴格來說,是復雜界面更適合VIPER,而不是大型app更適合VIPER。

至此,我的結論就是,快點擁抱VIPER的懷抱吧。

開始實踐

VIPER是2013年首次在iOS平臺上提出的設計,十分年輕,因此缺少大量參與者,以總結出更多最佳實踐。下一篇文章將會從VIPER的源頭開始,比較現有的各種VIPER實現,總結出一個我認為較好的實施方案。

iOS VIPER架構實踐(二):VIPER詳解與實現。里面有具體Demo。

參考

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

推薦閱讀更多精彩內容