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

黑超熊貓zuik發表於2017-08-21

簡介

最近半年在寫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進行資料轉換。其實在微軟的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詳解與實現。裡面有VIPER的具體Demo和程式碼模板。

參考

相關文章