因?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ù)綁定
或者delegation
和blocks
實(shí)現(xiàn).
什么是 VIPER透绩?
- View將用戶行為傳遞給presenter.
- Presenter將這些行為傳遞給interactor或router.
- 如果行為需要做計算操作翘骂,由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)模式有什么問題精耐,歡迎一起討論狼速。