VIPER 和 MVVM 到底有什麼區別

蘇大盒子發表於2019-03-04

這篇部落格主要的內容是譯自Göksel Köksal
Blurring the Lines Between MVVM and VIPER
(本文已獲得作者的授權翻譯),我把自己對於業務架構模式觀點放在了文末,以下是譯文:

如果你開發過移動端App,那你肯定聽說過 MVVM 和 VIPER. 雖然有觀點說MVVM的擴充套件性不夠好,也有觀點說VIPER是個過度設計的產物。而我在這裡想說的是,它倆非常接近,甚至我們都沒有必要去把它倆分開對待。

先來快速地過一遍 MVVM 和 VIPER.

什麼是 MVVM?
VIPER 和 MVVM 到底有什麼區別
  • View將使用者行為傳遞給view model.
  • View model處理這些行為並更新它們的狀態.
  • View model接著通知view, 這一步可以通過資料繫結或者delegationblocks實現.
什麼是 VIPER?
VIPER 和 MVVM 到底有什麼區別
  • View將使用者行為傳遞給presenter.
  • Presenter將這些行為傳遞給interactorrouter.
  • 如果行為需要做計算操作,由interactor處理並將狀態返回給presenter.
  • Presenter把這個狀態轉化為展示用的資料並更新view.
  • Router則封裝了導航邏輯,由presenter負責觸發.

想了解更多關於這兩種架構的內容,可以參考這篇牛逼的文章Bohdan OrloviOS Architecture Patterns*

我們的主要目標是什麼?

首要的目標是將UI和業務邏輯分離。這樣才可以在不破壞任何業務邏輯的情況下去更新UI,或者單獨地去測試業務邏輯的程式碼。事實上MVVM和VIPER都可以達到這個目標,只是方式不一樣而已。從這個角度來看的話,它倆的結構可以像下面這樣:

VIPER 和 MVVM 到底有什麼區別

MVVM的 UI 層只有一個 View 元件,而 VIPER 將 UI 層拆分成了三個元件:View, Presenter 和 Router. 而業務層顯然兩者基本差不多。
接下來我們通過例子看看他倆在 UI 層的區別。

一個虛構的App: TopMovies

假設我們要用 MVVM 做一個簡單的 App: 把 IMDB 上 TOP 25 的電影資料拉下來並顯示在一個列表中。 元件程式碼大概會是下面這樣:

protocol MovieListView: MovieListViewModelDelegate {
  private var viewModel: MovieListViewModel
  func updateWithMovies(_ movies: [Movie])
  func didTapOnReload()
  func didTapOnMovie(at index: Int)
  func showDetailView(for movie: Movie)
}

protocol MovieListViewModelDelegate: class {
  func viewModelDidUpdate(_ model: MovieListViewModel)
}

protocol MovieListViewModel {
  weak var delegate: MovieListViewModelDelegate? { get set }
  var movies: [Movie] { get }
  func fetchMovies()
}
複製程式碼
資料流:
  • View 把自己作為 view model 的 delegate.
  • 使用者點選並過載.
  • View 呼叫 view model 的 fetchMovies 方法.
  • 資料獲取成功後,view model 通知 delegate(view).
  • 呼叫updateWithMovies 並將電影物件轉化為展示用的資料顯示到列表上。

相當簡單的一個邏輯對吧。接下來我們在 macOS 上建立一個基本相同的 App, 並儘可能多地複用程式碼。

假設場景:實現 macOS 版本

首先可以確定一件事,view 的類肯定是不一樣的。因此我們沒法複用 iOS App 中展示邏輯的程式碼。而 iOS 的 view 已經在updateWithMovies將電影物件轉化成了展示用的資料,所以想要複用這部分邏輯的就只能它抽出來。我們把建立展示用的資料的程式碼挪到一個介於 view 和 view model 之間的中間類裡, 這樣就能在 iOS 和 macOS 的 view 裡複用這部分程式碼了。
於是我們把這個中間類就叫 Presenter, 叫這個名字純屬偶然,和VIPER一毛關係都沒有~

protocol MovieListView: MovieListPresenterDelegate {
  private var presenter: MovieListPresenter
  func didTapOnReload()
  func didTapOnMovie(at index: Int)
  func showDetailView(for movie: Movie)
}

protocol MovieListPresenterDelegate {
  func updateWithMoviePresentations(_ movies: [MoviePresentation])
}

protocol MovieListPresenter: MovieListViewModelDelegate {
  private var viewModel: MovieListViewModel
  func reload()
  func presentation(from movie: Movie) -> MoviePresentation
}

protocol MovieListViewModelDelegate: class {
  func viewModelDidUpdate(_ model: MovieListViewModel)
}

protocol MovieListViewModel {
  weak var delegate: MovieListViewModelDelegate? { get set }
  var movies: [Movie] { get }
  func fetchMovies()
}
複製程式碼
資料流:
  • View 把自己作為 Presenter 的 delegate.
  • Presenter 把自己作為 view model 的 delegate.
  • 使用者點選並過載.
  • View 呼叫 presenter的 reload 方法.
  • Presenter 呼叫 view model 的 fetchMovies 方法.
  • 資料獲取成功後,view model 通知 delegate(presenter).
  • 呼叫updateWithMovies 並將電影物件轉化為展示用的資料並通知 delegate(view).
  • View 更新自己.

這意味著我們可以通過讓任何 view 遵循 MovieListView 協議就能夠跨平臺實現上面的需求。
現在我們通過複用 iOS 專案大部分的程式碼實現了全新的 macOS App.
然而這個時候,蘋果宣佈了一個大事。。。

假設場景:iOS 重設計
VIPER 和 MVVM 到底有什麼區別

幾周後,蘋果釋出了iOS 26,Jone Ive 又雙叒叕宣佈了一個全新的設計系統。 我們的設計師看了以後賊興奮並且也很快就搞了一套全新的設計稿出來。現在我們的工作變成了實現這套全新的UI,並確保可以用A/B testing來控制只讓一部分使用者顯示這套UI。
我們這麼優秀的工程師,這點改動不算啥對吧。我們只需要寫一個新的 iOS view 並遵循 MovieListView 協議,然後繫結 presenter 就行了,簡直不要太簡單。

protocol MovieListView: MovieListPresenterDelegate {
  ...
  func didTapOnMovie(at index: Int)
  func showDetailView(for movie: Movie)
}
複製程式碼

在實現這個新類的時候,我們會意識到showDetailView在新舊view的實現是一樣的。我們可能會想到複製貼上這部分程式碼,不過我們這麼優秀的工程師,怎麼可能允許複製貼上程式碼對吧?
OK,我們把這部分邏輯也挪出來,並且把這個元件叫 Router, 同樣,這個名字也是純屬偶然。

protocol MovieListRouter {
  func showDetailView(for movie: Movie)
}
複製程式碼

Router 作為當前頁面的代言人,負責在需要的時候顯示對應的詳情頁。但是這個元件應該放在哪呢?放在新舊兩版view裡嗎?聽上去也可以不過就以往經驗來看,除非確實需求發生變化,還是不要頻繁改變 view 的程式碼比較好。
還是讓我們把這個責任交給 presenter 吧,讓它來持有 router. 這樣當使用者行為發生,presenter 接收到這個事件時,它可以決定是呼叫 view model 來做計算還是呼叫 router 來實現導航的功能。
現在我們把導航的邏輯也複用了,可以發版啦。
我們一起看看最終的程式碼結構:

protocol MovieListView: MovieListPresenterDelegate {
  private var presenter: MovieListPresenter
  func didTapOnReload()
  func didTapOnMovie(at index: Int)
}

protocol MovieListPresenterDelegate {
  func updateWithMoviePresentations(_ movies: [MoviePresentation])
}

protocol MovieListPresenter: MovieListViewModelDelegate {
  private var router: MovieListRouter
  private var viewModel: MovieListViewModel
  func reload()
  func presentation(from movie: Movie) -> MoviePresentation
}

protocol MovieListRouter {
  func showDetailView(for movie: Movie)
}

protocol MovieListViewModelDelegate: class {
  func viewModelDidUpdate(_ model: MovieListViewModel)
}

protocol MovieListViewModel {
  weak var delegate: MovieListViewModelDelegate? { get set }
  var movies: [Movie] { get }
  func fetchMovies()
}
複製程式碼

看到這裡,我想你應該 get 到了吧,這時候我們把 MovieListViewModel 改名為 MovieListInteractor的話, 程式碼就變成了 100%的VIPER,但同時又沒有違背 MVVM 的原則。

總結

軟體架構說白了就是一堆的規則。有的架構規則多,有的規則少。使用一種架構並不意味著就是完全摒棄另外一種。尤其是當我們在討論MVC, MVVM 和 VIPER的時候。

VIPER 和 MVVM 到底有什麼區別

從左到右,是一個擴充套件性的演化,而不是前後矛盾。VIPER 是這三者當中的最細化的版本,這也是為什麼很多人認為它是設計過度了,而且事實上我也覺得這些人的的批評是對的。
VIPER一共有5個元件,然而你卻不一定在所有場景裡都需要全部的5個元件。我認為我們在開發過程中應該把精力放在需求本身而不是盲目地去遵循一些設計規則。
對於 VIPER,我的建議是:

  • 從 VIPER 的簡化版開始,和 MVVM 基本差不多,只有 view, interactor 和 entities.
  • 如果你希望快速修改UI, 就把 presenter 加進來.
  • 如果你的專案裡有複雜且可重用的路由邏輯,那就新增 router.
  • 在實現每個需求之前,設計好類圖和介面。儘管業界普遍認為這樣做必要性不大但是絕對能幫你設計出更好的介面,並且最後來看能減少開發時間。

譯者的總結:

關於VIPER,我在之前一直有所耳聞,但是因為沒有在專案中實踐過,對於細節實際上是一知半解的。這篇文章從一個非常好的角度分析了VIPER和MVVM的區別,我看完後收益頗豐。因此在這裡將其翻譯為中文,以便自己日後回顧。

對於架構模式,我自己的觀點,和文中的觀點非常類似,我認為專案中選擇怎樣的架構模式根本不重要,我們的目的只有一個,那就是解耦且易擴充套件。

被業界diss無數次的MVC,實際上在優秀的程式設計師手裡,照樣能夠發揮得很好,但是到了一些相對初級的開發者那,則會有Massive Controller的問題,而這裡面最主要的原因,我認為就是MVC制定的規則太少了。

資深一些的開發者,他們對軟體架構的原則瞭解於心,因此不論架構模式的規則是多還是少,從他們手中產出的程式碼始終能維持在一個優雅的程度。因此,MVC在不同的人手中會有不同的結果。

而規則相對較多的MVVM,以及VIPER,在自身規則上做了更多的限制,使得不論什麼水平的開發者在遵循這些規則進行業務開發後,程式碼質量能夠保持在一個相對不錯的水平。

因此在我看來,選擇怎樣的架構模式取決於團隊的平均能力,大體上來說,團隊能力可以和架構模式的規則數量成反比。

對於業務的架構模式有什麼問題,歡迎一起討論。

相關文章