VIPER 和 MVVM 到底有什么區(qū)別

因?yàn)?a target="_blank" rel="nofollow">https://blog.csdn.net/urdfmqcul2/article/details/78788962
,博客搬家至https://juejin.im/user/59fd6315f265da4321536990

這篇博客主要的內(nèi)容是譯自G?ksel K?ksal
Blurring the Lines Between MVVM and VIPER
(本文已獲得作者的授權(quán)翻譯),我把自己對(duì)于業(yè)務(wù)架構(gòu)模式觀點(diǎn)放在了文末,以下是譯文:

如果你開發(fā)過移動(dòng)端App,那你肯定聽說過 MVVM 和 VIPER. 雖然有觀點(diǎn)說MVVM的擴(kuò)展性不夠好,也有觀點(diǎn)說VIPER是個(gè)過度設(shè)計(jì)的產(chǎn)物。而我在這里想說的是,它倆非常接近,甚至我們都沒有必要去把它倆分開對(duì)待。

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

什么是 MVVM?
  • View將用戶行為傳遞給view model.
  • View model處理這些行為并更新它們的狀態(tài).
  • View model接著通知view, 這一步可以通過數(shù)據(jù)綁定或者delegationblocks實(shí)現(xiàn).
什么是 VIPER?
  • View將用戶行為傳遞給presenter.
  • Presenter將這些行為傳遞給interactorrouter.
  • 如果行為需要做計(jì)算操作,由interactor處理并將狀態(tài)返回給presenter.
  • Presenter把這個(gè)狀態(tài)轉(zhuǎn)化為展示用的數(shù)據(jù)并更新view.
  • Router則封裝了導(dǎo)航邏輯,由presenter負(fù)責(zé)觸發(fā).

想了解更多關(guān)于這兩種架構(gòu)的內(nèi)容,可以參考這篇牛逼的文章Bohdan Orlov: iOS Architecture Patterns*

我們的主要目標(biāo)是什么?

首要的目標(biāo)是將UI和業(yè)務(wù)邏輯分離。這樣才可以在不破壞任何業(yè)務(wù)邏輯的情況下去更新UI,或者單獨(dú)地去測(cè)試業(yè)務(wù)邏輯的代碼。事實(shí)上MVVM和VIPER都可以達(dá)到這個(gè)目標(biāo),只是方式不一樣而已。從這個(gè)角度來看的話,它倆的結(jié)構(gòu)可以像下面這樣:



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

一個(gè)虛構(gòu)的App: TopMovies

假設(shè)我們要用 MVVM 做一個(gè)簡(jiǎn)單的 App: 把 IMDB 上 TOP 25 的電影數(shù)據(jù)拉下來并顯示在一個(gè)列表中。 組件代碼大概會(huì)是下面這樣:

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()
}
數(shù)據(jù)流:
  • View 把自己作為 view model 的 delegate.
  • 用戶點(diǎn)擊并重載.
  • View 調(diào)用 view model 的 fetchMovies 方法.
  • 數(shù)據(jù)獲取成功后,view model 通知 delegate(view).
  • 調(diào)用updateWithMovies 并將電影對(duì)象轉(zhuǎn)化為展示用的數(shù)據(jù)顯示到列表上。

相當(dāng)簡(jiǎn)單的一個(gè)邏輯對(duì)吧。接下來我們?cè)?macOS 上創(chuàng)建一個(gè)基本相同的 App, 并盡可能多地復(fù)用代碼。

假設(shè)場(chǎng)景:實(shí)現(xiàn) macOS 版本

首先可以確定一件事,view 的類肯定是不一樣的。因此我們沒法復(fù)用 iOS App 中展示邏輯的代碼。而 iOS 的 view 已經(jīng)在updateWithMovies將電影對(duì)象轉(zhuǎn)化成了展示用的數(shù)據(jù),所以想要復(fù)用這部分邏輯的就只能它抽出來。我們把創(chuàng)建展示用的數(shù)據(jù)的代碼挪到一個(gè)介于 view 和 view model 之間的中間類里, 這樣就能在 iOS 和 macOS 的 view 里復(fù)用這部分代碼了。
于是我們把這個(gè)中間類就叫 Presenter, 叫這個(gè)名字純屬偶然,和VIPER一毛關(guān)系都沒有~

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()
}
數(shù)據(jù)流:
  • View 把自己作為 Presenter 的 delegate.
  • Presenter 把自己作為 view model 的 delegate.
  • 用戶點(diǎn)擊并重載.
  • View 調(diào)用 presenter的 reload 方法.
  • Presenter 調(diào)用 view model 的 fetchMovies 方法.
  • 數(shù)據(jù)獲取成功后,view model 通知 delegate(presenter).
  • 調(diào)用updateWithMovies 并將電影對(duì)象轉(zhuǎn)化為展示用的數(shù)據(jù)并通知 delegate(view).
  • View 更新自己.

這意味著我們可以通過讓任何 view 遵循 MovieListView 協(xié)議就能夠跨平臺(tái)實(shí)現(xiàn)上面的需求。
現(xiàn)在我們通過復(fù)用 iOS 項(xiàng)目大部分的代碼實(shí)現(xiàn)了全新的 macOS App.
然而這個(gè)時(shí)候,蘋果宣布了一個(gè)大事。。。

假設(shè)場(chǎng)景:iOS 重設(shè)計(jì)


幾周后,蘋果發(fā)布了iOS 26,Jone Ive 又雙叒叕宣布了一個(gè)全新的設(shè)計(jì)系統(tǒng)。 我們的設(shè)計(jì)師看了以后賊興奮并且也很快就搞了一套全新的設(shè)計(jì)稿出來。現(xiàn)在我們的工作變成了實(shí)現(xiàn)這套全新的UI,并確保可以用A/B testing來控制只讓一部分用戶顯示這套UI。
我們這么優(yōu)秀的工程師,這點(diǎn)改動(dòng)不算啥對(duì)吧。我們只需要寫一個(gè)新的 iOS view 并遵循 MovieListView 協(xié)議,然后綁定 presenter 就行了,簡(jiǎn)直不要太簡(jiǎn)單。

protocol MovieListView: MovieListPresenterDelegate {
  ...
  func didTapOnMovie(at index: Int)
  func showDetailView(for movie: Movie)
}

在實(shí)現(xiàn)這個(gè)新類的時(shí)候,我們會(huì)意識(shí)到showDetailView在新舊view的實(shí)現(xiàn)是一樣的。我們可能會(huì)想到復(fù)制粘貼這部分代碼,不過我們這么優(yōu)秀的工程師,怎么可能允許復(fù)制粘貼代碼對(duì)吧?
OK,我們把這部分邏輯也挪出來,并且把這個(gè)組件叫 Router, 同樣,這個(gè)名字也是純屬偶然。

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

Router 作為當(dāng)前頁面的代言人,負(fù)責(zé)在需要的時(shí)候顯示對(duì)應(yīng)的詳情頁。但是這個(gè)組件應(yīng)該放在哪呢?放在新舊兩版view里嗎?聽上去也可以不過就以往經(jīng)驗(yàn)來看,除非確實(shí)需求發(fā)生變化,還是不要頻繁改變 view 的代碼比較好。
還是讓我們把這個(gè)責(zé)任交給 presenter 吧,讓它來持有 router. 這樣當(dāng)用戶行為發(fā)生,presenter 接收到這個(gè)事件時(shí),它可以決定是調(diào)用 view model 來做計(jì)算還是調(diào)用 router 來實(shí)現(xiàn)導(dǎo)航的功能。
現(xiàn)在我們把導(dǎo)航的邏輯也復(fù)用了,可以發(fā)版啦。
我們一起看看最終的代碼結(jié)構(gòu):

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()
}

看到這里,我想你應(yīng)該 get 到了吧,這時(shí)候我們把 MovieListViewModel 改名為 MovieListInteractor的話, 代碼就變成了 100%的VIPER,但同時(shí)又沒有違背 MVVM 的原則。

總結(jié)

軟件架構(gòu)說白了就是一堆的規(guī)則。有的架構(gòu)規(guī)則多,有的規(guī)則少。使用一種架構(gòu)并不意味著就是完全摒棄另外一種。尤其是當(dāng)我們?cè)谟懻揗VC, MVVM 和 VIPER的時(shí)候。



從左到右,是一個(gè)擴(kuò)展性的演化,而不是前后矛盾。VIPER 是這三者當(dāng)中的最細(xì)化的版本,這也是為什么很多人認(rèn)為它是設(shè)計(jì)過度了,而且事實(shí)上我也覺得這些人的的批評(píng)是對(duì)的。
VIPER一共有5個(gè)組件,然而你卻不一定在所有場(chǎng)景里都需要全部的5個(gè)組件。我認(rèn)為我們?cè)陂_發(fā)過程中應(yīng)該把精力放在需求本身而不是盲目地去遵循一些設(shè)計(jì)規(guī)則。
對(duì)于 VIPER,我的建議是:

  • 從 VIPER 的簡(jiǎn)化版開始,和 MVVM 基本差不多,只有 view, interactor 和 entities.
  • 如果你希望快速修改UI, 就把 presenter 加進(jìn)來.
  • 如果你的項(xiàng)目里有復(fù)雜且可重用的路由邏輯,那就添加 router.
  • 在實(shí)現(xiàn)每個(gè)需求之前,設(shè)計(jì)好類圖和接口。盡管業(yè)界普遍認(rèn)為這樣做必要性不大但是絕對(duì)能幫你設(shè)計(jì)出更好的接口,并且最后來看能減少開發(fā)時(shí)間。

譯者的總結(jié):

關(guān)于VIPER,我在之前一直有所耳聞,但是因?yàn)闆]有在項(xiàng)目中實(shí)踐過,對(duì)于細(xì)節(jié)實(shí)際上是一知半解的。這篇文章從一個(gè)非常好的角度分析了VIPER和MVVM的區(qū)別,我看完后收益頗豐。因此在這里將其翻譯為中文,以便自己日后回顧。
對(duì)于架構(gòu)模式,我自己的觀點(diǎn),和文中的觀點(diǎn)非常類似,我認(rèn)為項(xiàng)目中選擇怎樣的架構(gòu)模式根本不重要,我們的目的只有一個(gè),那就是解耦且易擴(kuò)展。
被業(yè)界diss無數(shù)次的MVC,實(shí)際上在優(yōu)秀的程序員手里,照樣能夠發(fā)揮得很好,但是到了一些相對(duì)初級(jí)的開發(fā)者那,則會(huì)有Massive Controller的問題,而這里面最主要的原因,我認(rèn)為就是MVC制定的規(guī)則太少了。
資深一些的開發(fā)者,他們對(duì)軟件架構(gòu)的原則了解于心,因此不論架構(gòu)模式的規(guī)則是多還是少,從他們手中產(chǎn)出的代碼始終能維持在一個(gè)優(yōu)雅的程度。因此,MVC在不同的人手中會(huì)有不同的結(jié)果。
而規(guī)則相對(duì)較多的MVVM,以及VIPER,在自身規(guī)則上做了更多的限制,使得不論什么水平的開發(fā)者在遵循這些規(guī)則進(jìn)行業(yè)務(wù)開發(fā)后,代碼質(zhì)量能夠保持在一個(gè)相對(duì)不錯(cuò)的水平。
因此在我看來,選擇怎樣的架構(gòu)模式取決于團(tuán)隊(duì)的平均能力,大體上來說,團(tuán)隊(duì)能力可以和架構(gòu)模式的規(guī)則數(shù)量成反比。

對(duì)于業(yè)務(wù)的架構(gòu)模式有什么問題,歡迎一起討論。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,570評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評(píng)論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,786評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,219評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,438評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,971評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,796評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,995評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,230評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評(píng)論 1 286
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,697評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,991評(píng)論 2 374

推薦閱讀更多精彩內(nèi)容