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

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

對代碼風格和架構有興趣的同學,肯定都已經(jīng)在很多地方見過各種架構的介紹。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內(nèi)部管理的數(shù)據(jù)模型,和各種數(shù)據(jù)操作工具
  • Controller負責將Model的變化更新到View
  • Controller負責處理來自View的事件

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

蘋果的MVC

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

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

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

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

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

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

MVP

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

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

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

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

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

MVVM

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

最普遍的MVVM

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

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

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

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

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

ViewModel的職責

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

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

ViewModel到底是什么?從它的命名和最初的設計來看,它只是View的抽象,目的是方便和Model進行數(shù)據(jù)轉換。而默認把業(yè)務邏輯也放到ViewModel里,大概是由于objc.io上那篇文章的影響。其實在微軟的WPF和前端里,MVVM的業(yè)務邏輯大部分是放在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),把業(yè)務邏輯放到了Presenter里。Presenter的引入讓ViewModel專注于View的抽象,和Model分離開來,只負責管理View相關的狀態(tài)、傳遞View的事件,因此ViewModel中的代碼可以得到很好的復用。而Presenter負責大部分業(yè)務邏輯,如果模塊需要重用,則把業(yè)務邏輯中的數(shù)據(jù)操作邏輯(domain logic)單獨分離出來作為重用代碼,其他的無法重用的應用邏輯(application logic)則依舊放在Presenter里。

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

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

VIPER

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

VIPER

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

各部分職責如下:

View

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

Presenter

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

Router

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

Interactor

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

Entity

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

和MVX的區(qū)別

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

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

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

優(yōu)點

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

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

缺點

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

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

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

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

總結

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

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

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

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

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

開始實踐

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

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

參考

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

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

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