VIPER 和 MVVM 到底有什麼區別

這篇博客主要的內容是譯自Göksel KöksalBlurring the Lines Between MVVM and VIPER (本文已得到做者的受權翻譯),我把本身對於業務架構模式觀點放在了文末,如下是譯文:ios

若是你開發過移動端App,那你確定據說過 MVVM 和 VIPER. 雖然有觀點說MVVM的擴展性不夠好,也有觀點說VIPER是個過分設計的產物。而我在這裏想說的是,它倆很是接近,甚至咱們都沒有必要去把它倆分開對待。程序員

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

什麼是 MVVM?

  • View將用戶行爲傳遞給view model.
  • View model處理這些行爲並更新它們的狀態.
  • View model接着通知view, 這一步能夠經過數據綁定或者delegationblocks實現.
什麼是 VIPER?

  • View將用戶行爲傳遞給presenter.
  • Presenter將這些行爲傳遞給interactorrouter.
  • 若是行爲須要作計算操做,由interactor處理並將狀態返回給presenter.
  • Presenter把這個狀態轉化爲展現用的數據並更新view.
  • Router則封裝了導航邏輯,由presenter負責觸發.

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

咱們的主要目標是什麼?

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

MVVM的 UI 層只有一個 View 組件,而 VIPER 將 UI 層拆分紅了三個組件:View, Presenter 和 Router. 而業務層顯然二者基本差很少。 接下來咱們經過例子看看他倆在 UI 層的區別。

一個虛構的App: TopMovies

假設咱們要用 MVVM 作一個簡單的 App: 把 IMDB 上 TOP 25 的電影數據拉下來並顯示在一個列表中。 組件代碼大概會是下面這樣:post

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一毛關係都沒有~fetch

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. 然而這個時候,蘋果宣佈了一個大事。。。spa

假設場景:iOS 重設計

幾周後,蘋果發佈了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 是這三者當中的最細化的版本,這也是爲何不少人認爲它是設計過分了,並且事實上我也以爲這些人的的批評是對的。 VIPER一共有5個組件,然而你卻不必定在全部場景裏都須要所有的5個組件。我認爲咱們在開發過程當中應該把精力放在需求自己而不是盲目地去遵循一些設計規則。 對於 VIPER,個人建議是:

  • 從 VIPER 的簡化版開始,和 MVVM 基本差很少,只有 view, interactor 和 entities.
  • 若是你但願快速修改UI, 就把 presenter 加進來.
  • 若是你的項目裏有複雜且可重用的路由邏輯,那就添加 router.
  • 在實現每一個需求以前,設計好類圖和接口。儘管業界廣泛認爲這樣作必要性不大可是絕對能幫你設計出更好的接口,而且最後來看能減小開發時間。

譯者的總結:

關於VIPER,我在以前一直有所耳聞,可是由於沒有在項目中實踐過,對於細節其實是隻知其一;不知其二的。這篇文章從一個很是好的角度分析了VIPER和MVVM的區別,我看完後收益頗豐。所以在這裏將其翻譯爲中文,以便本身往後回顧。

對於架構模式,我本身的觀點,和文中的觀點很是相似,我認爲項目中選擇怎樣的架構模式根本不重要,咱們的目的只有一個,那就是解耦且易擴展。

被業界diss無數次的MVC,實際上在優秀的程序員手裏,照樣可以發揮得很好,可是到了一些相對初級的開發者那,則會有Massive Controller的問題,而這裏面最主要的緣由,我認爲就是MVC制定的規則太少了。

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

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

所以在我看來,選擇怎樣的架構模式取決於團隊的平均能力,大致上來講,團隊能力能夠和架構模式的規則數量成反比。

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

相關文章
相關標籤/搜索