iOS 架構模式 - 簡述 MVC, MVP, MVVM 和 VIPER (譯)

OS 架構模式 - 簡述 MVC, MVP, MVVM 和 VIPER (譯)

Make everything as simple as possible, but not simpler?—?Albert Einstein

把每件事,做簡單到極致,但又不過于簡單 - 阿爾伯特·愛因斯坦

在使用 iOS 的 MVC 時候感覺怪怪的?想要嘗試下 MVVM?之前聽說過 VIPER,但是又糾結是不是值得去學?

繼續閱讀,你就會知道上面問題的答案 - 如果讀完了還是不知道的話,歡迎留言評論。

iOS 上面的架構模式你可能之前就了解過一些,接下來我們會幫你把它們進行一下梳理。我們先簡要回顧一下目前比較主流的架構模式,分析比較一些他們的原理,并用一些小栗子來進行練習。如果你對其中的某一種比較感興趣的話,我們也在文章里面給出了對應的鏈接。

對于設計模式的學習是一件容易上癮的事情,所以先提醒你一下:在你讀完這篇文章之后,可能會比讀之前有更多的疑問,比如:

(MVC)誰來負責網絡請求:是 Model 還是 Controller?

(MVVM)我該怎么去把一個 Model 傳遞給一個新創建的 View 的 ViewModel?

(VIPER)誰來負責創建 VIPER 模塊:是 Router 還是 Presenter?

為何要在意架構的選擇呢?

因為如果你不在意的話,難保一天,你就需要去調試一個巨大無比又有著各種問題的類,然后你會發現在這個類里面,你完全就找不到也修復不了任何 bug。一般來說,把這么大的一個類作為整體放在腦子里記著是一件非常困難的事情,你總是難免會忘掉一些比較重要的細節。如果你發現在你的應用里面已經開始出現這種狀況了,那你很可能遇到過下面這類問題:

這個類是一個 UIViewController 的子類。

你的數據直接保存在了 UIViewController 里面。

你的 UIViews 好像什么都沒做。

你的 Model 只是一個純粹的數據結構

你的單元測試什么都沒有覆蓋到

其實即便你遵循了 Apple 的設計規范,實現了Apple 的 MVC 框架,也還是一樣會遇到上面這些問題;所以也沒什么好失落的。Apple 的 MVC 框架有它自身的缺陷,不過這個我們后面再說。

讓我們先來定義一下好的框架應該具有的特征:

用嚴格定義的角色,平衡的將職責劃分給不同的實體。

可測性通常取決于上面說的第一點(不用太擔心,如果架構何時的話,做到這點并不難)。

易用并且維護成本低。

為什么要劃分?

當我們試圖去理解事物的工作原理的時候,劃分可以減輕我們的腦部壓力。如果你覺得開發的越多,大腦就越能適應去處理復雜的工作,確實是這樣。但是大腦的這種能力不是線性提高的,而且很快就會達到一個瓶頸。所以要處理復雜的事情,最好的辦法還是在遵循單一責任原則的條件下,將它的職責劃分到多個實體中去。

為什么要可測性?

對于那些對單元測試心存感激的人來說,應該不會有這方面的疑問:單元測試幫助他們測試出了新功能里面的錯誤,或者是幫他們找出了重構的一個復雜類里面的 bug。這意味著這些單元測試幫助這些開發者們在程序運行之前就發現了問題,這些問題如果被忽視的話很可能會提交到用戶的設備上去;而修復這些問題,又至少需要一周左右的時間(AppStore 審核)。

為什么要易用

這塊沒什么好說的,直說一點:最好的代碼是那些從未被寫出來的代碼。代碼寫的越少,問題就越少;所以開發者想少寫點代碼并不一定就是因為他懶。還有,當你想用一個比較聰明的方法的時候,全完不要忽略了它的維護成本。

MV(X) 的基本要素

現在我們面對架構設計模式的時候有了很多選擇:

MVC

MVP

MVVM

VIPER

首先前三種模式都是把所有的實體歸類到了下面三種分類中的一種:

Models(模型)—?數據層,或者負責處理數據的數據接口層。比如PersonPersonDataProvider

Views(視圖)- 展示層(GUI)。對于 iOS 來說所有以UI開頭的類基本都屬于這層。

Controller/Presenter/ViewModel(控制器/展示器/視圖模型)- 它是ModelView之間的膠水或者說是中間人。一般來說,當用戶對View有操作時它負責去修改相應Model;當Model的值發生變化時它負責去更新對應View

將實體進行分類之后我們可以:

更好的理解

重用(主要是 View 和 Model)

對它們獨立的進行測試

讓我從MV(X)系列開始講起,最后講VIPER

MVC - 它原來的樣子

在開始討論 Apple 的 MVC 之前,我們先來看下傳統的 MVC

在這種架構下,View 是無狀態的,在 Model 變化的時候它只是簡單的被 Controller 重繪;就像網頁一樣,點擊了一個新的鏈接,整個網頁就重新加載。盡管這種架構可以在 iOS 應用里面實現,但是由于 MVC 的三種實體被緊密耦合著,每一種實體都和其他兩種有著聯系,所以即便是實現了也沒有什么意義。這種緊耦合還戲劇性的減少了它們被重用的可能,這恐怕不是你想要在自己的應用里面看到的。綜上,傳統 MVC 的例子我覺得也沒有必要去寫了。

傳統的 MVC 已經不適合當下的 iOS 開發了。

Apple 的 MVC

理想

View 和 Model 之間是相互獨立的,它們只通過 Controller 來相互聯系。有點惱人的是 Controller 是重用性最差的,因為我們一般不會把冗雜的業務邏輯放在 Model 里面,那就只能放在 Controller 里了。

理論上看這么做貌似挺簡單的,但是你有沒有覺得有點不對勁?你甚至聽過有人把 MVC 叫做重控制器模式。另外關于 ViewController 瘦身已經成為 iOS 開發者們熱議的話題了。為什么 Apple 要沿用只是做了一點點改進的傳統 MVC 架構呢?

現實

Cocoa MVC 鼓勵你去寫重控制器是因為 View 的整個生命周期都需要它去管理,Controller 和 View 很難做到相互獨立。雖然你可以把控制器里的一些業務邏輯和數據轉換的工作交給 Model,但是你再想把負擔往 View 里面分攤的時候就沒辦法了;因為 View 的主要職責就只是講用戶的操作行為交給 Controller 去處理而已。于是 ViewController 最終就變成了所有東西的代理和數據源,甚至還負責網絡請求的發起和取消,還有...剩下的你來講。

像下面這種代碼你應該不陌生吧:

var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell

userCell.configureWithUser(user)

Cell 作為一個 View 直接用 Model 來完成了自身的配置,MVC 的原則被打破了,這種情況一直存在,而且還沒人覺得有什么問題。如果你是嚴格遵循 MVC 的話,你應該是在 ViewController 里面去配置 Cell,而不是直接將 Model 丟給 Cell,當然這樣會讓你的 ViewController 更重。

Cocoa MVC 被戲稱為重控制器模式還是有原因的。

問題直到開始單元測試(希望你的項目里面已經有了)之后才開始顯現出來。Controller 測試起來很困難,因為它和 View 耦合的太厲害,要測試它的話就需要頻繁的去 mock View 和 View 的生命周期;而且按照這種架構去寫控制器代碼的話,業務邏輯的代碼也會因為視圖布局代碼的原因而變得很散亂。

我們來看下面這段 playground 中的例子:

import UIKitstruct Person { // Modellet firstName: Stringlet lastName: String}class GreetingViewController : UIViewController { // View + Controllervar person: Person!let showGreetingButton = UIButton()let greetingLabel = UILabel()override func viewDidLoad() {super.viewDidLoad()self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)}func didTapButton(button: UIButton) {let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastNameself.greetingLabel.text = greeting}// layout code goes here}// Assembling of MVClet model = Person(firstName: "David", lastName: "Blaine")let view = GreetingViewController()view.person = model;

MVC 的組裝,可以放在當前正在顯示的 ViewController 里面

這段代碼看起來不太好測試對吧?我們可以把greeting的生成方法放到一個新類GreetingModel里面去單獨測試。但是我們如果不調用與 View 相關的方法的話 (viewDidLoad, didTapButton),就測試不到GreetingViewController里面任何的顯示邏輯(雖然在上面這個例子里面,邏輯已經很少了);而調用的話就可能需要把所有的 View 都加載出來,這對單元測試來說太不利了。

實際上,在模擬器(比如 iPhone 4S)上運行并測試 View 的顯示并不能保證在其他設備上(比如 iPad)也能良好運行。所以我建議把「Host Application」從你的單元測試配置項里移除掉,然后在不啟動模擬器的情況下去跑你的單元測試。

View 和 Controller 之間的交互,并不能真正的被單元測試覆蓋

綜上所述,Cocoa MVC 貌似并不是一個很好的選擇。但是我們還是評估一下他在各方面的表現(在文章開頭有講):

劃分- View 和 Model 確實是實現了分離,但是 View 和 Controller 耦合的太厲害

可測性- 因為劃分的不夠清楚,所以能測的基本就只有 Model 而已

易用- 相較于其他模式,它的代碼量最少。而且基本上每個人都很熟悉它,即便是沒太多經驗的開發者也能維護。

在這種情況下你可以選擇 Cocoa MVC:你并不想在架構上花費太多的時間,而且你覺得對于你的小項目來說,花費更高的維護成本只是浪費而已。

如果你最看重的是開發速度,那么 Cocoa MVC 就是你最好的選擇。

MVP - 保證了職責劃分的(promises delivered) Cocoa MVC

看起來確實很像 Apple 的 MVC 對吧?確實蠻像,它的名字是MVP(被動變化的 View)。稍等...這個意思是說 Apple 的 MVC 實際上是 MVP 嗎?不是的,回想一下,在 MVC 里面 View 和 Controller 是耦合緊密的,但是對于 MVP 里面的 Presenter 來講,它完全不關注 ViewController 的生命周期,而且 View 也能被簡單 mock 出來,所以在 Presenter 里面基本沒什么布局相關的代碼,它的職責只是通過數據和狀態更新 View。

如果我跟你講 UIViewController 在這里的角色其實是 View 你感覺如何。

在 MVP 架構里面,UIViewController 的那些子類其實是屬于 View 的,而不是 Presenter。這種區別提供了極好的可測性,但是這是用開發速度的代價換來的,因為你必須要手動的去創建數據和綁定事件,像下面這段代碼中做的一樣:

import UIKitstruct Person { // Modellet firstName: Stringlet lastName: String}protocol GreetingView: class {func setGreeting(greeting: String)}protocol GreetingViewPresenter {init(view: GreetingView, person: Person)func showGreeting()}class GreetingPresenter : GreetingViewPresenter {unowned let view: GreetingViewlet person: Personrequired init(view: GreetingView, person: Person) {self.view = viewself.person = person}func showGreeting() {let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastNameself.view.setGreeting(greeting)}}class GreetingViewController : UIViewController, GreetingView {var presenter: GreetingViewPresenter!let showGreetingButton = UIButton()let greetingLabel = UILabel()override func viewDidLoad() {super.viewDidLoad()self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)}func didTapButton(button: UIButton) {self.presenter.showGreeting()}func setGreeting(greeting: String) {self.greetingLabel.text = greeting}// layout code goes here}// Assembling of MVPlet model = Person(firstName: "David", lastName: "Blaine")let view = GreetingViewController()let presenter = GreetingPresenter(view: view, person: model)view.presenter = presenter

關于組裝方面的重要說明

MVP 架構擁有三個真正獨立的分層,所以在組裝的時候會有一些問題,而 MVP 也成了第一個披露了這種問題的架構。因為我們不想讓 View 知道 Model 的信息,所以在當前的 ViewController(角色其實是 View)里面去進行組裝肯定是不正確的,我們應該在另外的地方完成組裝。比如,我們可以創建一個應用層(app-wide)的 Router 服務,讓它來負責組裝和 View-to-View 的轉場。這個問題不僅在 MVP 中存在,在接下來要介紹的模式里面也都有這個問題。

讓我們來看一下 MVP 在各方面的表現:

劃分- 我們把大部分的職責都分配到了 Presenter 和 Model 里面,而 View 基本上不需要做什么(在上面的例子里面,Model 也什么都沒做)。

可測性- 簡直棒,我們可以通過 View 來測試大部分的業務邏輯。

易用- 就我們上面那個簡單的例子來講,代碼量差不多是 MVC 架構的兩倍,但是 MVP 的思路還是蠻清晰的。

MVP 架構在 iOS 中意味著極好的可測性和巨大的代碼量。

MVP - 添加了數據綁定的另一個版本

還存在著另一種的 MVP - Supervising Controller MVP。這個版本的 MVP 包括了 View 和 Model 的直接綁定,與此同時 Presenter(Supervising Controller)仍然繼續處理 View 上的用戶操作,控制 View 的顯示變化。

但是我們之前講過,模糊的職責劃分是不好的事情,比如 View 和 Model 的緊耦合。這個道理在 Cocoa 桌面應用開發上面也是一樣的。

就像傳統 MVC 架構一樣,我找不到有什么理由需要為這個有瑕疵的架構寫一個例子。

MVVM - 是 MV(X) 系列架構里面最新興的,也是最出色的

MVVM架構是 MV(X) 里面最新的一個,讓我們希望它在出現的時候已經考慮到了 MV(X) 模式之前所遇到的問題吧。

理論上來說,Model - View - ViewModel 看起來非常棒。View 和 Model 我們已經都熟悉了,中間人的角色我們也熟悉了,但是在這里中間人的角色變成了 ViewModel。

它跟 MVP 很像:

MVVM 架構把 ViewController 看做 View。

View 和 Model 之間沒有緊耦合

另外,它還像 Supervising 版的 MVP 那樣做了數據綁定,不過這次不是綁定 View 和 Model,而是綁定 View 和 ViewModel。

那么,iOS 里面的 ViewModel 到底是個什么東西呢?本質上來講,他是獨立于 UIKit 的, View 和 View 的狀態的一個呈現(representation)。ViewModel 能主動調用對 Model 做更改,也能在 Model 更新的時候對自身進行調整,然后通過 View 和 ViewModel 之間的綁定,對 View 也進行對應的更新。

綁定

我在 MVP 的部分簡單的提過這個內容,在這里讓我們再延伸討論一下。綁定這個概念源于 OS X 平臺的開發,但是在 iOS 平臺上面,我們并沒有對應的開發工具。當然,我們也有 KVO 和 通知,但是用這些方式去做綁定不太方便。

那么,如果我們不想自己去寫他們的話,下面提供了兩個選擇:

選一個基于 KVO 的綁定庫,比如RZDataBinding或者SwiftBond

使用全量級的函數式響應編程框架,比如ReactiveCocoaRxSwift或者PromiseKit

實際上,現在提到「MVVM」你應該就會想到 ReactiveCocoa,反過來也是一樣。雖然我們可以通過簡單的綁定來實現 MVVM 模式,但是 ReactiveCocoa(或者同類型的框架)會讓你更大限度的去理解 MVVM。

響應式編程框架也有一點不好的地方,能力越大責任越大嘛。用響應式編程用得不好的話,很容易會把事情搞得一團糟。或者這么說,如果有什么地方出錯了,你需要花費更多的時間去調試。看著下面這張調用堆棧圖感受一下:

在接下來的這個小例子中,用響應式框架(FRF)或者 KVO 都顯得有點大刀小用,所以我們用另一種方式:直接的調用 ViewModel 的showGreeting方法去更新自己(的greeting屬性),(在greeting屬性的didSet回調里面)用greetingDidChange閉包函數去更新 View 的顯示。

import UIKitstruct Person { // Modellet firstName: Stringlet lastName: String}protocol GreetingViewModelProtocol: class {var greeting: String? { get }var greetingDidChange: ((GreetingViewModelProtocol) -> ())? { get set } // function to call when greeting did changeinit(person: Person)func showGreeting()}class GreetingViewModel : GreetingViewModelProtocol {let person: Personvar greeting: String? {didSet {self.greetingDidChange?(self)}}var greetingDidChange: ((GreetingViewModelProtocol) -> ())?required init(person: Person) {self.person = person}func showGreeting() {self.greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName}}class GreetingViewController : UIViewController {var viewModel: GreetingViewModelProtocol! {didSet {self.viewModel.greetingDidChange = { [unowned self] viewModel inself.greetingLabel.text = viewModel.greeting}}}let showGreetingButton = UIButton()let greetingLabel = UILabel()override func viewDidLoad() {super.viewDidLoad()self.showGreetingButton.addTarget(self.viewModel, action: "showGreeting", forControlEvents: .TouchUpInside)}// layout code goes here}// Assembling of MVVMlet model = Person(firstName: "David", lastName: "Blaine")let viewModel = GreetingViewModel(person: model)let view = GreetingViewController()view.viewModel = viewModel

然后,我們再回過頭來對它各方面的表現做一個評價:

劃分- 這在我們的小栗子里面表現的不是很清楚,但是 MVVM 框架里面的 View 比 MVP 里面負責的事情要更多一些。因為前者是通過 ViewModel 的數據綁定來更新自身狀態的,而后者只是把所有的事件統統交給 Presenter 去處理就完了,自己本身并不負責更新。

可測性- 因為 ViewModel 對 View 是一無所知的,這樣我們對它的測試就變得很簡單。View 應該也是能夠被測試的,但是可能因為它對 UIKit 的依賴,你會直接略過它。

易用- 在我們的例子里面,它的代碼量基本跟 MVP 持平,但是在實際的應用當中 MVVM 會更簡潔一些。因為在 MVP 下你必須要把 View 的所有事件都交給 Presenter 去處理,而且需要手動的去更新 View 的狀態;而在 MVVM 下,你只需要用綁定就可以解決。

MVVM 真的很有魅力,因為它不僅結合了上述幾種框架的優點,還不需要你為視圖的更新去寫額外的代碼(因為在 View 上已經做了數據綁定),另外它在可測性上的表現也依然很棒。

VIPER - 把搭建樂高積木的經驗應用到 iOS 應用的設計上

VIPER是我們最后一個要介紹的框架,這個框架比較有趣的是它不屬于任何一種 MV(X) 框架。

到目前為止,你可能覺得我們把職責劃分成三層,這個顆粒度已經很不錯了吧。現在 VIPER 從另一個角度對職責進行了劃分,這次劃分了五層

Interactor(交互器)- 包括數據(Entities)或者網絡相關的業務邏輯。比如創建新的 entities 或者從服務器上獲取數據;要實現這些功能,你可能會用到一些服務和管理(Services and Managers):這些可能會被誤以為成是外部依賴東西,但是它們就是 VIPER 的 Interactor 模塊。

Presenter(展示器)- 包括 UI(but UIKit independent)相關的業務邏輯,可以調用 Interactor 中的方法。

Entities(實體)- 純粹的數據對象。不包括數據訪問層,因為這是 Interactor 的職責。

Router(路由)- 負責 VIPER 模塊之間的轉場

實際上 VIPER 模塊可以只是一個頁面(screen),也可以是你應用里整個的用戶使用流程(the whole user story)- 比如說「驗證」這個功能,它可以只是一個頁面,也可以是連續相關的一組頁面。你的每個「樂高積木」想要有多大,都是你自己來決定的。

如果我們把 VIPER 和 MV(X) 系列做一個對比的話,我們會發現它們在職責劃分上面有下面的一些區別:

Model(數據交互)的邏輯被轉移到了 Interactor 里面,Entities 只是一個什么都不用做的數據結構體。

Controller/Presenter/ViewModel的職責里面,只有 UI 的展示功能被轉移到了 Presenter 里面。Presenter 不具備直接更改數據的能力。

VIPER 是第一個把導航的職責單獨劃分出來的架構模式,負責導航的就是Router層。

如何正確的使用導航(doing routing)對于 iOS 應用開發來說是一個挑戰,MV(X) 系列的架構完全就沒有意識到(所以也不用處理)這個問題。

下面的這個列子并沒有涉及到導航和 VIPER 模塊間的轉場,同樣上面 MV(X) 系列架構里面也都沒有涉及。

import UIKitstruct Person { // Entity (usually more complex e.g. NSManagedObject)let firstName: Stringlet lastName: String}struct GreetingData { // Transport data structure (not Entity)let greeting: Stringlet subject: String}protocol GreetingProvider {func provideGreetingData()}protocol GreetingOutput: class {func receiveGreetingData(greetingData: GreetingData)}class GreetingInteractor : GreetingProvider {weak var output: GreetingOutput!func provideGreetingData() {let person = Person(firstName: "David", lastName: "Blaine") // usually comes from data access layerlet subject = person.firstName + " " + person.lastNamelet greeting = GreetingData(greeting: "Hello", subject: subject)self.output.receiveGreetingData(greeting)}}protocol GreetingViewEventHandler {func didTapShowGreetingButton()}protocol GreetingView: class {func setGreeting(greeting: String)}class GreetingPresenter : GreetingOutput, GreetingViewEventHandler {weak var view: GreetingView!var greetingProvider: GreetingProvider!func didTapShowGreetingButton() {self.greetingProvider.provideGreetingData()}func receiveGreetingData(greetingData: GreetingData) {let greeting = greetingData.greeting + " " + greetingData.subjectself.view.setGreeting(greeting)}}class GreetingViewController : UIViewController, GreetingView {var eventHandler: GreetingViewEventHandler!let showGreetingButton = UIButton()let greetingLabel = UILabel()override func viewDidLoad() {super.viewDidLoad()self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)}func didTapButton(button: UIButton) {self.eventHandler.didTapShowGreetingButton()}func setGreeting(greeting: String) {self.greetingLabel.text = greeting}// layout code goes here}// Assembling of VIPER module, without Routerlet view = GreetingViewController()let presenter = GreetingPresenter()let interactor = GreetingInteractor()view.eventHandler = presenterpresenter.view = viewpresenter.greetingProvider = interactorinteractor.output = presenter

我們再來評價下它在各方面的表現:

劃分- 毫無疑問的,VIPER 在職責劃分方面是做的最好的。

可測性- 理所當然的,職責劃分的越好,測試起來就越容易

易用- 最后,你可能已經猜到了,上面兩點好處都是用維護性的代價換來的。一個小小的任務,可能就需要你為各種類寫大量的接口。

那么,我們到底應該給「樂高」一個怎樣的評價呢?

如果你在使用 VIPER 框架的時候有一種在用樂高積木搭建帝國大廈的感覺,那么你可能正在犯錯誤;可能對于你負責的應用來說,還沒有到使用 VIPER 的時候,你應該把一些事情考慮的再簡單一些。總是有一些人忽視這個問題,繼續扛著大炮去打小鳥。我覺得可能是因為他們相信,雖然目前來看維護成本高的不合常理,但是至少在將來他們的應用可以從 VIPER 架構上得到回報吧。如果你也跟他們的觀點一樣的話,那我建議你嘗試一下Generamba- 一個可以生成 VIPER 框架的工具。雖然對于我個人來講,這感覺就像給大炮裝上了一個自動瞄準系統,然后去做一件只用彈弓就能解決的事情。

結論

我們簡單了解了幾種架構模式,對于那些讓你困惑的問題,我希望你已經找到了答案。但是毫無疑問,你應該已經意識到了,在選擇架構模式這件問題上面,不存在什么銀色子彈,你需要做的就是具體情況具體分析,權衡利弊而已。

因此在同一個應用里面,即便有幾種混合的架構模式也是很正常的一件事情。比如:開始的時候,你用的是 MVC 架構,后來你意識到有一個特殊的頁面用 MVC 做的的話維護起來會相當的麻煩;這個時候你可以只針對這一個頁面用 MVVM 模式去開發,對于之前那些用 MVC 就能正常工作的頁面,你完全沒有必要去重構它們,因為兩種架構是完全可以和睦共存的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容