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)翻譯)毡泻,我把自己對于業(yè)務(wù)架構(gòu)模式觀點(diǎn)放在了文末先馆,以下是譯文:

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

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

什么是 MVVM?
  • View將用戶行為傳遞給view model.
  • View model處理這些行為并更新它們的狀態(tài).
  • View model接著通知view, 這一步可以通過數(shù)據(jù)綁定或者delegationblocks實(shí)現(xiàn).
什么是 VIPER透绩?
  • View將用戶行為傳遞給presenter.
  • Presenter將這些行為傳遞給interactorrouter.
  • 如果行為需要做計算操作翘骂,由interactor處理并將狀態(tài)返回給presenter.
  • Presenter把這個狀態(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ú)地去測試業(yè)務(wù)邏輯的代碼莹桅。事實(shí)上MVVM和VIPER都可以達(dá)到這個目標(biāo),只是方式不一樣而已。從這個角度來看的話诈泼,它倆的結(jié)構(gòu)可以像下面這樣:



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

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

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

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 并將電影對象轉(zhuǎn)化為展示用的數(shù)據(jù)顯示到列表上瓮孙。

相當(dāng)簡單的一個邏輯對吧唐断。接下來我們在 macOS 上創(chuàng)建一個基本相同的 App, 并盡可能多地復(fù)用代碼。

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

首先可以確定一件事杭抠,view 的類肯定是不一樣的脸甘。因此我們沒法復(fù)用 iOS App 中展示邏輯的代碼。而 iOS 的 view 已經(jīng)在updateWithMovies將電影對象轉(zhuǎn)化成了展示用的數(shù)據(jù)偏灿,所以想要復(fù)用這部分邏輯的就只能它抽出來丹诀。我們把創(chuàng)建展示用的數(shù)據(jù)的代碼挪到一個介于 view 和 view model 之間的中間類里, 這樣就能在 iOS 和 macOS 的 view 里復(fù)用這部分代碼了翁垂。
于是我們把這個中間類就叫 Presenter, 叫這個名字純屬偶然铆遭,和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 并將電影對象轉(zhuǎn)化為展示用的數(shù)據(jù)并通知 delegate(view).
  • View 更新自己.

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

假設(shè)場景:iOS 重設(shè)計


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

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

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

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

Router 作為當(dāng)前頁面的代言人咧栗,負(fù)責(zé)在需要的時候顯示對應(yīng)的詳情頁窄赋。但是這個組件應(yīng)該放在哪呢?放在新舊兩版view里嗎楼熄?聽上去也可以不過就以往經(jīng)驗(yàn)來看忆绰,除非確實(shí)需求發(fā)生變化,還是不要頻繁改變 view 的代碼比較好可岂。
還是讓我們把這個責(zé)任交給 presenter 吧错敢,讓它來持有 router. 這樣當(dāng)用戶行為發(fā)生,presenter 接收到這個事件時缕粹,它可以決定是調(diào)用 view model 來做計算還是調(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 到了吧,這時候我們把 MovieListViewModel 改名為 MovieListInteractor的話, 代碼就變成了 100%的VIPER绘面,但同時又沒有違背 MVVM 的原則欺税。

總結(jié)

軟件架構(gòu)說白了就是一堆的規(guī)則。有的架構(gòu)規(guī)則多揭璃,有的規(guī)則少晚凿。使用一種架構(gòu)并不意味著就是完全摒棄另外一種。尤其是當(dāng)我們在討論MVC, MVVM 和 VIPER的時候瘦馍。



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

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

譯者的總結(jié):

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市卦停,隨后出現(xiàn)的幾起案子向胡,更是在濱河造成了極大的恐慌,老刑警劉巖惊完,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件僵芹,死亡現(xiàn)場離奇詭異,居然都是意外死亡小槐,警方通過查閱死者的電腦和手機(jī)淮捆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來本股,“玉大人攀痊,你說我怎么就攤上這事≈粝裕” “怎么了苟径?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長躬审。 經(jīng)常有香客問我棘街,道長,這世上最難降的妖魔是什么承边? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任遭殉,我火速辦了婚禮,結(jié)果婚禮上博助,老公的妹妹穿的比我還像新娘险污。我一直安慰自己,他們只是感情好富岳,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布蛔糯。 她就那樣靜靜地躺著,像睡著了一般窖式。 火紅的嫁衣襯著肌膚如雪蚁飒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天萝喘,我揣著相機(jī)與錄音淮逻,去河邊找鬼琼懊。 笑死,一個胖子當(dāng)著我的面吹牛爬早,可吹牛的內(nèi)容都是我干的肩碟。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼凸椿,長吁一口氣:“原來是場噩夢啊……” “哼削祈!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起脑漫,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤髓抑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后优幸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吨拍,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年网杆,在試婚紗的時候發(fā)現(xiàn)自己被綠了羹饰。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,841評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡碳却,死狀恐怖队秩,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情昼浦,我是刑警寧澤馍资,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站关噪,受9級特大地震影響鸟蟹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜使兔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一建钥、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧虐沥,春花似錦熊经、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至盯荤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間焕盟,已是汗流浹背秋秤。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工宏粤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人灼卢。 一個月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓绍哎,卻偏偏與公主長得像,于是被迫代替她去往敵國和親鞋真。 傳聞我的和親對象是個殘疾皇子崇堰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評論 2 354

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