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

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

一: 簡介

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

對代碼風(fēng)格和架構(gòu)有興趣的同學(xué),肯定都已經(jīng)在很多地方見過各種架構(gòu)的介紹。MVCMVP、MVVM、VIPER,細(xì)分程度逐漸上升。這些架構(gòu)設(shè)計都是來自MVC,只是各自用不同的方式對MVC進(jìn)行了細(xì)分,在此只對MVC、MVP和MVVM作精簡介紹,想要詳細(xì)了解可以參考這些文章:

iOS 架構(gòu)模式–解密 MVC,MVP,MVVM以及VIPER架構(gòu)

淺談 MVC、MVP 和 MVVM 架構(gòu)模式

二: MVC

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

  • View是展示給外部的界面
  • Model是Controller內(nèi)部管理的數(shù)據(jù)模型
  • Controller負(fù)責(zé)將Model的變化更新到View; Controller負(fù)責(zé)處理來自View的事件.

MVC的劃分粒度很粗,因此有很多種具體實現(xiàn),各個實現(xiàn)有差異,因此并沒有一個十分明確的標(biāo)準(zhǔn)定義。

三:蘋果的MVC

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

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

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

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

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

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

四: MVP

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

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

在iOS里,UIView和UIViewController共同組合成了MVP中的View。UIView負(fù)責(zé)元素的展示,UIViewController負(fù)責(zé)界面布局和組合,并把事件轉(zhuǎn)發(fā)給Presenter。
因此在MVP里,業(yè)務(wù)邏輯被放到了Presenter中,由它負(fù)責(zé)協(xié)調(diào)View和Model。而由于View的抽離,Presenter的狀態(tài)是可控的,在測試時更不容易受外部影響。

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

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

五: MVVM

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

5.1: 最普遍的MVVM

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

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

5.2: 數(shù)據(jù)綁定

利用ReactiveCocoa和RxSwift這些函數(shù)式響應(yīng)編程框架實現(xiàn)數(shù)據(jù)綁定,可以用很少的代碼完成復(fù)雜的業(yè)務(wù)邏輯,熟練時能夠提升開發(fā)速度。但是數(shù)據(jù)綁定的缺點也很明顯:調(diào)試?yán)щy,數(shù)據(jù)來源難以回溯,在線上出bug的時候就很難追蹤了,所以從這方面來說又降低了維護(hù)的效率。

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

5.3: ViewModel的職責(zé)

但是除了數(shù)據(jù)綁定,MVVM還有另一個問題。把業(yè)務(wù)邏輯放到ViewModel中,雖然能夠為UIViewController減負(fù),但是只是把問題轉(zhuǎn)移了,最終ViewModel還是會變成另一個Massive ViewModel

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

ViewModel到底是什么?從它的命名和最初的設(shè)計來看,它只是View的抽象,目的是方便和Model進(jìn)行數(shù)據(jù)轉(zhuǎn)換。其實在微軟的WPF和前端里,MVVM的業(yè)務(wù)邏輯大部分是放在Model層的,相關(guān)的討論可以參考:

MVVM: ViewModel and Business Logic Connection

Where does business logic sit in MVVM?

The Problems with MVVM on iOS

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

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

那么如何對Controller進(jìn)行進(jìn)一步的職責(zé)細(xì)分呢?答案就是VIPER。

六: VIPER

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

[圖片上傳失敗...(image-ab9aa7-1573097998703)]

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

各部分職責(zé)如下:

View

  • 提供完整的視圖,負(fù)責(zé)視圖的組合、布局、更新
  • 向Presenter提供更新視圖的接口
  • 將View相關(guān)的事件發(fā)送給Presenter

Presenter

  • 接收并處理來自View的事件
  • 向Interactor請求調(diào)用業(yè)務(wù)邏輯
  • 向Interactor提供View中的數(shù)據(jù)
  • 接收并處理來自Interactor的數(shù)據(jù)回調(diào)事件
  • 通知View進(jìn)行更新操作
  • 通過Router跳轉(zhuǎn)到其他View

Router

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

Interactor

  • 維護(hù)主要的業(yè)務(wù)邏輯功能,向Presenter提供現(xiàn)有的業(yè)務(wù)用例
  • 維護(hù)、獲取、更新Entity
  • 當(dāng)有業(yè)務(wù)相關(guān)的事件發(fā)生時,處理事件,并通知Presenter

Entity

  • 和Model一樣的數(shù)據(jù)模型

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

  • View事件管理
  • 數(shù)據(jù)事件管理
  • 事件和業(yè)務(wù)的轉(zhuǎn)化
  • 總結(jié)每個業(yè)務(wù)用例
  • 模塊內(nèi)分層隔離
  • 模塊間通信

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

優(yōu)點
VIPER的特色就是職責(zé)明確,粒度細(xì),隔離關(guān)系明確,這樣能帶來很多優(yōu)點:

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

缺點

  • 一個模塊內(nèi)的類數(shù)量增大,代碼量增大,在層與層之間需要花更多時間設(shè)計接口。
    使用代碼模板來自動生成文件和模板代碼可以減少很多重復(fù)勞動,而花費時間設(shè)計和編寫接口是減少耦合的路上不可避免的,你也可以使用數(shù)據(jù)綁定這樣的技術(shù)來減少一些傳遞的層次。

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

總結(jié)
有人可能會覺得,一個界面模塊真的有必要使用這么復(fù)雜的架構(gòu)嗎?這樣是不是過度設(shè)計?
我反對這種觀點。不要被VIPER的組織圖嚇到,VIPER并不復(fù)雜,它是將原來MVC中的Controller中的各種任務(wù)進(jìn)行了清晰的分解,在寫代碼時,你會很清楚你正在做什么。事實上,它比使用了數(shù)據(jù)綁定技術(shù)的MVVM更加簡單,就是因為它職責(zé)明確。從MVC轉(zhuǎn)到VIPER的過程同樣是很清晰的,它甚至把重構(gòu)的思路都體現(xiàn)出來了。而MVVM則留下了許多尚未明確的責(zé)任,導(dǎo)致不同的人會在某些地方有不同的實現(xiàn)。即便你還在使用MVC,你也應(yīng)該在Controller中分離出VIPER總結(jié)出的那些專項職責(zé),既然如此,為何不徹底地明確這些職責(zé),把它們分散到不同的文件中呢?一旦開始這樣的工作,你就已經(jīng)向VIPER靠攏了。
有人可能會覺得,VIPER適合大型app,中小型app沒必要過早使用。
我反對這種觀點。VIPER是單個界面模塊內(nèi)的架構(gòu)設(shè)計,并不是整個app架構(gòu)層面的設(shè)計,和app的整體架構(gòu)沒有多大的關(guān)系,也不存在過早使用VIPER的情況。所以,嚴(yán)格來說,是復(fù)雜界面更適合VIPER,而不是大型app更適合VIPER。
至此,我的結(jié)論就是,快點擁抱VIPER的懷抱吧。。

開始實踐
VIPER是2013年首次在iOS平臺上提出的設(shè)計,十分年輕,因此缺少大量參與者,以總結(jié)出更多最佳實踐。下一篇文章將會從VIPER的源頭開始,比較現(xiàn)有的各種VIPER實現(xiàn),總結(jié)出一個我認(rèn)為較好的實施方案。
地址:iOS VIPER架構(gòu)實踐(二):VIPER詳解與實現(xiàn)。里面有VIPER的具體Demo和代碼模板。

參考資料
iOS 架構(gòu)模式–解密 MVC,MVP,MVVM以及VIPER架構(gòu)
淺談 MVC、MVP 和 MVVM 架構(gòu)模式

轉(zhuǎn)載:iOS VIPER架構(gòu)實踐(一):從MVC到MVVM到VIPER

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

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

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