iOS VIPER架構(gòu)實(shí)踐(一):從MVC到MVVM到VIPER
一: 簡介
最近半年在寫app的時(shí)候,研究了一下各種iOS代碼架構(gòu),最后選擇了VIPER進(jìn)行實(shí)踐,在此對實(shí)踐中遇到的各種設(shè)計(jì)問題做一番總結(jié),并分享造出的輪子。
對代碼風(fēng)格和架構(gòu)有興趣的同學(xué),肯定都已經(jīng)在很多地方見過各種架構(gòu)的介紹。MVC、MVP、MVVM、VIPER,細(xì)分程度逐漸上升。這些架構(gòu)設(shè)計(jì)都是來自MVC,只是各自用不同的方式對MVC進(jìn)行了細(xì)分,在此只對MVC、MVP和MVVM作精簡介紹,想要詳細(xì)了解可以參考這些文章:
iOS 架構(gòu)模式–解密 MVC,MVP,MVVM以及VIPER架構(gòu)
二: MVC
Model-View-Controller。MVC簡單地將一個(gè)模塊分為3部分:
- View是展示給外部的界面
- Model是Controller內(nèi)部管理的數(shù)據(jù)模型
- Controller負(fù)責(zé)將Model的變化更新到View; Controller負(fù)責(zé)處理來自View的事件.
MVC的劃分粒度很粗,因此有很多種具體實(shí)現(xiàn),各個(gè)實(shí)現(xiàn)有差異,因此并沒有一個(gè)十分明確的標(biāo)準(zhǔn)定義。
三:蘋果的MVC
蘋果的Cocoa Touch就遵照了MVC的設(shè)計(jì),一個(gè)界面分為UIView和UIViewController,UIView負(fù)責(zé)渲染和接收觸摸事件,UIViewController負(fù)責(zé)子view之間的布局、組合、更新以及事件處理。
盡管蘋果已經(jīng)給我們提供了簡單的MVC支持,但是在實(shí)踐中我們卻常常沒有遵守MVC。原因在于Cocoa Touch中的Model部分是由我們自己負(fù)責(zé)管理的,并沒有提供原生的設(shè)計(jì)支持。所以有時(shí)候會出現(xiàn)這樣的情況:一個(gè)UIView為了方便,提供了一個(gè)從某個(gè)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耦合,幾乎無法重用。而且在測試的時(shí)候,很難做到單獨(dú)測試沒有View的那部分代碼,因?yàn)樵趯懙臅r(shí)候就很容易將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í)例上設(shè)置的,這樣就很容易把源界面和目的界面耦合起來,簡單地把界面跳轉(zhuǎn)的部分單獨(dú)抽離為一個(gè)封裝好的跳轉(zhuǎn)方法可以一定程度上減少這部分耦合,但也不可避免地會多寫許多代碼。
因此,蘋果的MVC,實(shí)際上是Model-View-ViewController。它是一個(gè)視圖驅(qū)動的設(shè)計(jì),Controller只是為了管理View而存在的。蘋果把UIViewController和Model的關(guān)系設(shè)計(jì)交給了我們自己。所以,如何把一個(gè)UIViewController進(jìn)行更明確的分工,就是這些架構(gòu)要做的事。
四: MVP
Model-View-Presenter用一個(gè)Presenter,把Controller中View的部分剔除,實(shí)現(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)是可控的,在測試時(shí)更不容易受外部影響。
在iOS中使用MVP很簡單,在View和Presenter之間用protocol做好事件傳遞就可以。缺點(diǎn)就是多了一層用于隔離的接口,會導(dǎo)致代碼數(shù)量增大。
但是隨著界面越來越復(fù)雜,Presenter中的業(yè)務(wù)代碼也會越來越龐大,總有一天會遇到一個(gè)新的問題:如何再細(xì)分Presenter。
五: MVVM
Model-View-ViewModel模式,它也和MVP一樣,目的是解決View和Model的耦合。各部分分工如下:
5.1: 最普遍的MVVM
- Model提供數(shù)據(jù)模型
- View負(fù)責(zé)視圖展示
- ViewModel用于描述View的狀態(tài),例如View的顏色、顯示的文字等屬性類的信息,將View抽象成了一個(gè)特殊的模型,并且持有和管理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)編程框架實(shí)現(xiàn)數(shù)據(jù)綁定,可以用很少的代碼完成復(fù)雜的業(yè)務(wù)邏輯,熟練時(shí)能夠提升開發(fā)速度。但是數(shù)據(jù)綁定的缺點(diǎn)也很明顯:調(diào)試?yán)щy,數(shù)據(jù)來源難以回溯,在線上出bug的時(shí)候就很難追蹤了,所以從這方面來說又降低了維護(hù)的效率。
其實(shí)數(shù)據(jù)綁定只是一種為了減少膠水代碼的技術(shù)實(shí)現(xiàn)方式,MVVM的設(shè)計(jì)并沒有要求必須要使用數(shù)據(jù)綁定,你也完全可以使用protocol的方式來將ViewModel的變化傳遞給View,讓數(shù)據(jù)流向更清晰。MVVM的關(guān)鍵是將View進(jìn)行了抽象,從而實(shí)現(xiàn)View和Model的解耦。
5.3: ViewModel的職責(zé)
但是除了數(shù)據(jù)綁定,MVVM還有另一個(gè)問題。把業(yè)務(wù)邏輯放到ViewModel中,雖然能夠?yàn)閁IViewController減負(fù),但是只是把問題轉(zhuǎn)移了,最終ViewModel還是會變成另一個(gè)Massive ViewModel
而且當(dāng)ViewModel維護(hù)Model和業(yè)務(wù)邏輯時(shí),可復(fù)用性就會大大降低。例如把同一個(gè)登錄界面復(fù)用到另一個(gè)app中時(shí),login model中的屬性名或者類型很可能會改變,從而數(shù)據(jù)處理的方式也會改變,導(dǎo)致ViewModel無法重用。而當(dāng)View由多個(gè)子View組成時(shí),ViewModel里也會引入多個(gè)子ViewModel,這就又導(dǎo)致了View的實(shí)現(xiàn)影響了ViewModel的實(shí)現(xiàn)。奇怪的是,國內(nèi)iOS圈對這個(gè)問題的探討十分稀少。
ViewModel到底是什么?從它的命名和最初的設(shè)計(jì)來看,它只是View的抽象,目的是方便和Model進(jìn)行數(shù)據(jù)轉(zhuǎn)換。其實(shí)在微軟的WPF和前端里,MVVM的業(yè)務(wù)邏輯大部分是放在Model層的,相關(guān)的討論可以參考:
MVVM: ViewModel and Business Logic Connection
Where does business logic sit in MVVM?
而針對這個(gè)問題,有人又提出了一個(gè)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)單獨(dú)分離出來作為重用代碼,其他的無法重用的應(yīng)用邏輯(application logic)則依舊放在Presenter里。
和MVP相比,MVVM用了一種更優(yōu)雅的方式來抽象View。但它和MVP其實(shí)是類似的,只做了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多出了兩個(gè)東西: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的各個(gè)模塊
Interactor
- 維護(hù)主要的業(yè)務(wù)邏輯功能,向Presenter提供現(xiàn)有的業(yè)務(wù)用例
- 維護(hù)、獲取、更新Entity
- 當(dāng)有業(yè)務(wù)相關(guān)的事件發(fā)生時(shí),處理事件,并通知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ù)邏輯實(shí)現(xiàn)代碼都放在了Interactor里。Interactor的設(shè)計(jì)里提出了"用例"的概念,也就是把每一個(gè)會出現(xiàn)的業(yè)務(wù)流程封裝好,這樣可測試性會大大提高。而Router則進(jìn)一步解決了不同模塊之間的耦合。所以,VIPER和上面幾個(gè)MVX相比,多總結(jié)出了幾個(gè)需要維護(hù)的東西:
- View事件管理
- 數(shù)據(jù)事件管理
- 事件和業(yè)務(wù)的轉(zhuǎn)化
- 總結(jié)每個(gè)業(yè)務(wù)用例
- 模塊內(nèi)分層隔離
- 模塊間通信
而這里面,還可以進(jìn)一步細(xì)分一些職責(zé)。VIPER實(shí)際上已經(jīng)把Controller的概念淡化了,這拆分出來的幾個(gè)部分,都有很明確的單一職責(zé),有些部分之間是完全隔絕的,在開發(fā)時(shí)就應(yīng)該清晰地區(qū)分它們各自的職責(zé),而不是將它們視為一個(gè)Controller。
優(yōu)點(diǎn)
VIPER的特色就是職責(zé)明確,粒度細(xì),隔離關(guān)系明確,這樣能帶來很多優(yōu)點(diǎn):
- 可測試性好。UI測試和業(yè)務(wù)邏輯測試可以各自單獨(dú)進(jìn)行。
- 易于迭代。各部分遵循單一職責(zé),可以很明確地知道新的代碼應(yīng)該放在哪里。
- 隔離程度高,耦合程度低。一個(gè)模塊的代碼不容易影響到另一個(gè)模塊。
- 易于團(tuán)隊(duì)合作。各部分分工明確,團(tuán)隊(duì)合作時(shí)易于統(tǒng)一代碼風(fēng)格,可以快速接手別人的代碼
缺點(diǎn)
一個(gè)模塊內(nèi)的類數(shù)量增大,代碼量增大,在層與層之間需要花更多時(shí)間設(shè)計(jì)接口。
使用代碼模板來自動生成文件和模板代碼可以減少很多重復(fù)勞動,而花費(fèi)時(shí)間設(shè)計(jì)和編寫接口是減少耦合的路上不可避免的,你也可以使用數(shù)據(jù)綁定這樣的技術(shù)來減少一些傳遞的層次。模塊的初始化較為復(fù)雜,打開一個(gè)新的界面需要生成View、Presenter、Interactor,并且設(shè)置互相之間的依賴關(guān)系。而iOS中缺少這種設(shè)置復(fù)雜初始化的原生方式。
總結(jié)
有人可能會覺得,一個(gè)界面模塊真的有必要使用這么復(fù)雜的架構(gòu)嗎?這樣是不是過度設(shè)計(jì)?
我反對這種觀點(diǎn)。不要被VIPER的組織圖嚇到,VIPER并不復(fù)雜,它是將原來MVC中的Controller中的各種任務(wù)進(jìn)行了清晰的分解,在寫代碼時(shí),你會很清楚你正在做什么。事實(shí)上,它比使用了數(shù)據(jù)綁定技術(shù)的MVVM更加簡單,就是因?yàn)樗氊?zé)明確。從MVC轉(zhuǎn)到VIPER的過程同樣是很清晰的,它甚至把重構(gòu)的思路都體現(xiàn)出來了。而MVVM則留下了許多尚未明確的責(zé)任,導(dǎo)致不同的人會在某些地方有不同的實(shí)現(xiàn)。即便你還在使用MVC,你也應(yīng)該在Controller中分離出VIPER總結(jié)出的那些專項(xiàng)職責(zé),既然如此,為何不徹底地明確這些職責(zé),把它們分散到不同的文件中呢?一旦開始這樣的工作,你就已經(jīng)向VIPER靠攏了。
有人可能會覺得,VIPER適合大型app,中小型app沒必要過早使用。
我反對這種觀點(diǎn)。VIPER是單個(gè)界面模塊內(nèi)的架構(gòu)設(shè)計(jì),并不是整個(gè)app架構(gòu)層面的設(shè)計(jì),和app的整體架構(gòu)沒有多大的關(guān)系,也不存在過早使用VIPER的情況。所以,嚴(yán)格來說,是復(fù)雜界面更適合VIPER,而不是大型app更適合VIPER。
至此,我的結(jié)論就是,快點(diǎn)擁抱VIPER的懷抱吧。。
開始實(shí)踐
VIPER是2013年首次在iOS平臺上提出的設(shè)計(jì),十分年輕,因此缺少大量參與者,以總結(jié)出更多最佳實(shí)踐。下一篇文章將會從VIPER的源頭開始,比較現(xiàn)有的各種VIPER實(shí)現(xiàn),總結(jié)出一個(gè)我認(rèn)為較好的實(shí)施方案。
地址:iOS VIPER架構(gòu)實(shí)踐(二):VIPER詳解與實(shí)現(xiàn)。里面有VIPER的具體Demo和代碼模板。
參考資料
iOS 架構(gòu)模式–解密 MVC,MVP,MVVM以及VIPER架構(gòu)
淺談 MVC、MVP 和 MVVM 架構(gòu)模式