[譯]iOS架構模式——解密MVC、MVP、MVVM和VIPER

爲避免撕逼,提早聲明:本文純屬翻譯,僅僅是爲了學習,加上水平有限,見諒!react

【原文】https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52ios

使用MVC進行iOS開發感受到很怪異?在切換到MVVM的時候心存疑慮?據說過VIPER,可是不知道是否值得采用? 讀下去,這篇文章將爲你一一解惑。 若是你正打算組織一下在iOS環境下你掌握的架構模式知識體系。咱們接下來回簡單地回顧幾個流行的架構並作幾個小的練習。關於某個例子若是你想了解的更詳細一些,能夠查看下方的連接。git

掌握設計模式會讓人沉迷其中,全部必定要小心:相比閱讀本文章以前,你可能會問更多像這樣的問題: 由誰進行網絡請求:Model?仍是ViewController?github

如何向新視圖(View)的ViewModel中傳遞Modelweb

由誰建立一個新的VIPER模塊:路由(Router)?仍是展現器(Presenter)? objective-c

...


爲何在意架構的選擇?

由於若是你不這樣作,終有一天,你在調試一個擁有着數十個不一樣方法和變量(things)的龐大的類文件時,你會發現你沒法找到並修復此文件中的任何問題。天然地,也很難把這個類文件當作一個總體而熟稔於心,這樣你可能老是會錯過一些重要的細節。若是你的應用已經處於這樣的境況,頗有多是這樣:編程

  • 這個類是UIViewController的子類。
  • 你的數據直接存儲在UIViewController中。
  • 你的UIViews什麼都不作。
  • Model是啞數據結構(dumb data structure )。

dumb data structure: 只用來存儲數據的結構,沒有任何方法。詳見:https://stackoverflow.com/questions/32944751/what-is-dumb-data-and-dumb-data-object-meanswift

  • 單元測試沒有0覆蓋。 即便你是按照蘋果的指導方針並實現蘋果的MVC模式,也會出現上述問題,全部不要難過。蘋果的MVC模式存在着一些些問題,這點咱們稍後再說。

讓咱們定義一下一個好的架構應該有的特色設計模式

  1. 能把代碼職責均衡的解耦到不一樣的功能類裏。(Balanced distribution of responsibilities among entities with strict roles.)
  2. 可測試性(Testability usually comes from the first feature.)。
  3. 易用、維護成本低(Ease of use and a low maintenance cost.)。

解耦( Why Distribution ?)

在咱們試弄清楚事物是如何運做的時候,解耦能夠保證大腦的負載均衡。若是你認爲開發的(項目)越多你的大腦越能適應理解複雜的問題,那麼你就是對的。可是這個能力不是線性擴展的而且很快就能達到上限。因此,解決複雜性的最簡單的方式就是在多個實體間按照「單一責任原則」 拆分職責。promise

可測試(Why Testability ?)

對於那些已經習慣了單元測試的人來講這並非一個問題,由於再添加了新的特性和重構了一個複雜的類後一般會運行失敗。這意味着單元測試能夠幫助開發者發現一些在運行時纔會出現的問題,而且這些問題常見於安裝在用戶的手機上的應用上,此外要修復這些問題也須要大概一週的時間。

易用(Why Ease of use ?)

這個問題並不須要回答,但,值得一提的是:最好的代碼老是那些沒有被寫出來的代碼。所以,代碼越少,錯誤也就越少。這也說明,總想着寫最少代碼的開發者不是由於他們懶,而且你也不該該由於一個更聰明的解決方案而忽視維護成本。


##MV(X)概要 現今,當咱們說起架構設計模式的時候,咱們有不少的選擇。好比:

  • MVC
  • MVP
  • MVVM
  • VIPER

上述的前三個架構採起的是,把應用中的實體(entities)放入下面三個類別中其中一個內。

  • Models——負責域數據(domain data)和操做數據的數據訪問層(Data access layer),可認爲"Person"和"PersonDataProvider"類。
  • Views——負責表現層(GUI),對於iOS環境來講就是全部以"UI"開頭的類。
  • Controller/Presenter/ViewModel——模型(Model)和視圖(View)的粘合劑、中介,一般的負責經過響應用戶在視圖(View)上的操做通知模型(Model)更新、並經過模型的改變來更新視圖(View)。

實體解耦能讓咱們:

  • 更好的理解它們(這點我麼已經知道)
  • 複用(大多用於視圖(View)和模型(Model))
  • 獨立測試

讓咱們想看一下MV(X)架構,以後再回過頭來看VIPER


MVC

MVC前世

在討論蘋果的MVC架構時,先來看一下傳統的MVC是什麼樣的。

Traditional MVC
在這種狀況中,視圖( View)是無狀態的。一旦模型( Model)改變視圖( View)僅僅只是被控制器( Controller)渲染而已。想象一下點擊一個連接導航到其餘地方後網頁徹底加載出來的狀況。儘管,iOS應用可使用傳統的MVC架構,但這並無多大意義,由於架構自己就存在問題——三個實體( entities)之間聯繫太過緊密,每個實體都知道(引用)另外兩個實體。這就致使了實體的複用性急劇降低——在你的應用中這並非你所想要的。出於這個緣由,咱們就不寫MVC範例了。

Traditional MVC doesn't seems to be applicable to modern iOS development. 傳統MVC架構看上去並不適合用於如今的iOS開發中。

Apple's MVC

預期

Expectation MVC

控制器(Controller)是視圖(View)與模型(Model)二者之間的中介,這使得視圖(View)與模型(Model)都不知道對方的存在。控制器(Controller)是可複用的最少的,對咱們來講這一般很好,由於咱們必須有一個地方去放置一些不適合放在模型(Model)中且比較棘手的邏輯。 理論上,看起來很是簡單,可是你感受到有些地方不對,是否是這樣?你甚至據說過人們把MVC稱做Massive View Controller。此外,視圖控制器"瘦身"(View Controller offloading)成了iOS開發者中的一個重要話題。若是蘋果只是採用傳統MVC架構或者只是稍加改進,爲何會出現這種狀況?

MVC此生(現實狀況)

Realistic Cocoa MVC
Cocoa MVC鼓勵你使用大型的視圖控制器( Massive View Controllers),因爲他們都參與到了視圖( View)的生命週期中了以致於很難說他們是分離的。儘管你仍有能力分流一些業務邏輯和數據轉換功能到模型( Model)中,可是當涉及到把工做分流到視圖( View)中去時你就誒有更多的選擇了,由於在大多數時候視圖( View)的全部職責是把動做傳遞到控制器( Controller)中。視圖控制器( View Controller)最終最爲全部控件的委託和數據源,一般負責調度和取消網絡請求...應有盡有

你見過多少次這樣的代碼:

var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell
userCell.configureWithUser(user)
複製代碼

cell這個視圖是由Model直接配置數據的,所以這違反了MVC指南,可是這種狀況無時無刻不在發生着,並且一般人們並不認爲這樣有什麼錯的。若是你嚴格的遵照MVC架構,那麼你應該在Controller中配置cell數據,不用把Model傳遞到View中去,這會增長控制器的大小(複雜度)。

Cocoa MVC is reasonably unabbreviated the Massive View Controller. Cocoa MVC 被稱做大型視圖控制器是合理的。

在未說起單元測試(Unit Testing)MVC的問題並非很明顯(但願,你的項目中有單元測試)。因爲你的視圖控制器(View Controller)與視圖(View)是緊耦合的,所以很難對其進行測試,由於你不得不很是有創造性的模擬視圖和他們的生命週期,使用這種方式編寫視圖控制器(View Controller)代碼,你要儘量的把業務邏輯和視圖佈局代碼分離開來。

讓咱們來看一個簡單地playground例子:

import UIKit
struct Person { // Model
	let firstName: String
	let lastName: String
}
class GreetingViewController: UIViewController {// View + Controller
	var person: Person!
	let showGreetingButton = UIButton()
	let greetingLabel = UILabel()
	
	override func viewDidLoad() {
		super.viewDidLoad()
		self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvent: .TouchUpInside)
	}
	func didTapButton(button: UIButton) {
		let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
		self.greetingLabel.text = greeting
	}
	
	// 佈局代碼在這兒
	......	
}
// 組合MVC
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
view.person = model
複製代碼

MVC assembling can be performed in the presenting view controller 組合MVC能夠在展現視圖控制器(presenting view controller)中來完成

不是很容易測試,是否是這樣?咱們能夠把生成greeting的代碼放入到GreetingModel類裏並單獨的進行測試,可是,在沒有直接調用與UIView有關的方法(如:viewDidLoad, didTapButton,這些方法可能會加載全部的視圖,不利於單元測試。)的狀況下,咱們沒法測試GreetingViewController中的任何展現邏輯(儘管上面的代碼中沒有太多這樣的邏輯)。

事實上,在模擬器(如:iPhone4s)上加載並測試視圖並不能保證在其餘設備(如:iPad)上也能正常工做,因此,我建議從Unit Test目標(Unit Test target)配置中移除主應用程序(Host Application)並在模擬器上沒有應用運行的狀況下運行測試。

The interactions between the View and the Controller aren't really testable with Unit Tests. 視圖和控制器之間的交互很難進行單元測試

綜上所述,Cocoa MVC 多是一個至關糟糕的選擇。讓咱們按照文章開頭定義的特色來評估一下這種架構模式:

  • 解耦(Distribution)——視圖(View)和模型(Model)確實解耦了,然而,視圖(View)和控制器(Controller)倒是緊密耦合的。
  • 可測試(Testability)——因爲緊耦合的關係,你只能測試視圖(Model)。
  • 易用(Ease of use)——同其餘模式相比代碼最少。此外,你們都熟悉它,所以,很用以掌握甚至是新手。

若是你沒有打算在架構時耗費太多時間而且以爲高成本的維護費用對你的小項目來講是一種過分的浪費的話,那麼Cocoa MVC就是你的最好選擇。

Cocoa MVC is the best architectural pattern in term of the speed of the development. 在開發速度上面Cocoa MVC是最好的架構模式。


MVP

Cocoa MVC’s promises delivered

Passive View variant of MVP
是否是很像蘋果的MVC架構?沒錯,確實如此,它就是 MVP(Passive View variant)。等下...是否是 Apple’s MVC事實上就是 MVP?並非,回想一下在 MVCViewController是緊密耦合的,然而,MVP的中介—— Presenter與View Controller的生命週期沒有任何關係,而且很容易模擬View,因此 Presenter中沒有任何佈局代碼,可是它卻負責用數據和狀態更新 View

What if i told you,the UIViewController is the View.

我告訴你,視圖控制器就是視圖。

MVP來講,UIViewController的子類實際上就是視圖(Views)不是展現器(Presenters)。這點區別帶來了極好的可測試性,而花費代價是開發速度,由於你不得不手工進行數據和事件綁定,就如你在下面的例子中看到的那樣:

import UIKit
struct Person { // Model
	let firstName: String
	let lastName: String
}

protocol GreetingView: class {
	func setGreeting(greeting: String)
}

protocol GreetingViewPresenter {
	init(view: GreetingView, person: Person)
	func showGreeting()
}

class GreetingPresenter: GreetingViewPresenter {
	unowned let view: GreetingView
	let person: Person
	required init(view: GreetingView, person: Person) {
		self.view = view
		self.person = person
	}
	
	func showGreeting() {
		let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
		self.view.setGreeting(greeting)
	}
}
class GreetingViewController: UIViewController, GreetingView {
	var presenter: GreetingViePresenter!
	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 go here
}

let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
let presenter = GreetingPresenter(view: view, person: model)
view.presenter = presenter
複製代碼

關於組裝的重要提示

MVP是第一個揭示出組裝問題(assembly problem)的架構模式,而出現這個問題的緣由是它有三個實際上獨立的層。因爲咱們不想讓視圖(View)瞭解模型(Model),因此在展現視圖控制器(也就是視圖)執行組裝是不正確的,所以咱們不得不在其餘地方執行它。例如,咱們能夠建立一個app範圍(app-wide)的路由(Router)服務,讓它來完成執行組裝和視圖到視圖(View-to-View)的展現功能。這個問題不止在MVP中存在,在下面介紹的其餘模式中也存在。

讓咱們看一下MVP的特色:

  • 解耦(Distribution)——咱們在最大程度上分離了展現器(Presenter)和模型(Model),還有至關簡單、輕薄的視圖(dumb view)(在上述例子中的模型也很簡單)。
  • 可測試性(Testability)——很棒,因爲簡單的視圖,咱們能夠測試大多數的業務邏輯。
  • 易用性(Easy of use)——在咱們簡單不完整的例子中,相比於MVC這些代碼成倍的增長了,可是與此同時,MVP模式的思路卻更加的清晰。

MVP in iOS means superb testability and a lot of code iOS中的MVP架構意味着極好的可測試性和大量的代碼。

####綁定和Hooters 還有一種類型的MVP架構模式——the Supervising Controller MVP。這個變種包括了視圖和模型的直接綁定,展現器(The Supervising Controller)在處理動做的同時還能夠改變視圖。

Supervising Presenter variant of the MVP

可是,就如咱們已經知道的,模糊的職責拆分是不正確的,視圖和模型的緊耦合也一樣不可取。這和Cocoa桌面應用開發很類似。和傳統的MVC同樣,給有瑕疵的架構寫例子沒有任何意義。


MVVM

MV(X)類中近期最優秀的架構(The latest and the greatest of the MV(X) kind)

MVVM是MV(X)這類中最新的架構形式,因此,咱們但願它可以解決MV(X)以前所面臨的問題。

理論上,Model-View_ViewModel這種架構很棒。不只ViewModel,並且Mediator——至關於View Model,咱們都已經熟悉。

MVVM
它和 MVP很類似:

  • MVVM把視圖控制器當作視圖。
  • 視圖(View)和模型(Model)之間不存在緊耦合。

另外,它還能夠像MVP那樣綁定;可是綁定不是發生在視圖(View)和模型(Model)之間,而是視圖(View)和視圖模型(View Model)之間。

那麼,iOS現實中的視圖模型(View Model)的廬山面目是什麼?它是你的視圖及其狀態的基本的UIKit的獨立展現。視圖模型觸發模型的改變,並利用改變後的Model更新本身,因爲咱們在視圖和視圖模型之間進行了綁定,視圖也會根據視圖模型的改變而改變。

綁定(Bindings)

綁定我在講解MVP架構部分簡單的提到過,這裏咱們在對其進行一些討論。綁定是從OSX開發而來的,並且iOS中並無這個概念。固然,咱們有KVO和通知(notifications),可是它的使用並無綁定方便。

因此,假若不想本身編寫綁定代碼,咱們還有兩個選擇:

事實上,現今,只要你聽到「MVVM」你就會想到ReactiveCocoa,反之亦然。儘管使用簡單地綁定也能夠建立MVVM架構的項目,可是,ReactiveCocoa(或者同類的庫)卻可讓你把使用MVVM架構的優點最大化。

關於Reactive庫有一個殘酷的現實須要面對:功能強大卻伴隨着巨大的職責。當使用Reactive庫的時候極容易把不少事情搞混,若是出現錯誤,你可能須要花費不少的時間去在APP中定位問題所在,因此看一下下圖的調用堆棧。

Reactive Debugging

在簡單的例子中,使用FRF(functional reactive function:函數式響應式函數)庫甚至KVO都顯得大材小用,相反咱們顯式使用*showGreeting方法讓視圖模型(View Model)更新,並使用greetingDidChange*回調函數這樣一個簡單地屬性。

import UIKit
struct Person { //Model
	let firstName: String
	let lastName: String
}
protocol GreetingViewModelProtocol: class {
	var greeting: String? {get}
	// function to call when greeting did change
	var greetingDidChange: ((GreetingViewModelProtocol) -> ()) ? (get set)
	init(person: Person)
	func showGreeting()
}
class GreetingViewModel: GreetingViewModelProtocol {
	let person: Person
	var 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 in self.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
}

let model = Person(firstName: "David", lastName: "Blaine")
let viewModel = GreetingViewModel(person: model)
let view = GreetingViewController()
view.viewModel = viewModel
複製代碼

再回過來看一下咱們的特色評估:

  • 解耦(Distribution)——在咱們上面的的簡例中可能不太明顯,事實上,MVVM的視圖比MVP的視圖擁有更多的職責。由於,前者經過綁定從視圖模型(ViewModel)更新本身,然後者則是把全部的事件前置到Presenter中,也不對本身的狀態進行更新。
  • 可測試性(Testability)——View ModelView一無所知,這可讓咱們輕易地對其進行測試。也能夠對視圖(View)測試,但因爲UIKit依賴,你可能想跳過她。
  • 易用性(Easy of use)——在咱們的例子中,MVVMMVP有一樣的代碼量,可是在實際的應用中,對於MVP你須要把全部事件從視圖(View)前置到展現器(Presenter)並手動的更新視圖,而對於MVVM,若是你使用了綁定則會變的很容易。

MVVM極其吸人眼球,它融合了上述全部架構的的優點,此外,因爲它在視圖(View)端進行了綁定,你能夠不須要任何額外的代碼對視圖(View)進行更新。雖然如此,可測試性依然保持在一個很好的層次。


###VIPER ####把搭建樂高積木的經驗拿到iOS應用設計中使用 VIPER使咱們最後的選擇,這種架構尤其有趣,由於他不是屬於MV(X)類的架構。

到目前爲止,關於職責粒度的劃分很是合理這點你確定贊同。VIPER在職責劃分上面又作了一次迭代,此次咱們一共有五層。

VIPER

  • 交互器(Interactor)——包含與數據(Entities)或者網絡相關的業務邏輯,向建立一個實體的對象或者從網絡獲取對象。爲了這個目的,你須要用到一些ServicesManagers,這些不能算是VIPER的一部分,更確切的說只是些外部依賴。
  • 展現器(Presenter)——包含與UI相關(可是獨立於UIKit)的業務邏輯,調用交互器(Interactor)中的方法。
  • 實體(Entities)——簡單地數據對象,並非數據訪問層,由於數據訪問是交互器(Interactor)的職責。
  • 路由(Router)——用來鏈接VIPER中的模塊。

大體上說,VIPER模塊能夠是一個界面,也能夠是整個應用的用戶界面(user story)——想象一下驗證功能,它能夠是一個界面也能夠是幾個相關聯的界面。」樂高積木「塊應該多大呢?——這取決你本身。

若是咱們同MV(X)這一類進行比較,咱們能夠看到幾個不一樣的職責解耦之處:

  • 模型(Model(數據交互(data interaction))邏輯轉移到了交互器Interactor)中,同時**實體(Entities)**做爲單一的數據結構存在。
  • 只有控制器/展現器/視圖模型的UI展現責任轉移到了展現器(Presenter),而不是數據修改功能。
  • **VIPER是第一個明確的負責導航功能的架構模式,這點是經過路由(Router)**來解決的。

在iOS應用中,尋一個適當的方式進行頁面路由是一個具備挑戰性的工做,而MV(X)這類模式只是簡單的避而不談。

這個例子沒有涉及到路由和模塊間的交互,由於,這些話題在MV(X)這類模式中也沒有說起。

import UIKIt
struct Person { // 實體(一般要比這個複雜,例如:NSManagedObject)
	let firstName: String
	let lastName: String
}
struct GreetingData { // 傳遞數據結構(不是實體)
	let greeting: String
	let subject: String
}

protocol GreetingOutput: class {
	func receiveGreetingData(greetingData: GreetingData)
}

class GreetingInteractor: GreetingProvider {
	weak var output: GreetingOutput!
	func provideGreetingData() {
		let person = Person(firstName: "David", lastName: "Blaine")// 一般來自於數據訪問層
		let subject = person.firstName + " " + person.lastName
		let 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.subject
		self.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
}

let view = GreetingViewController()
let presenter = GreetingPresenter()
let interactor = GreetingInteractor()
view.eventHandler = presenter
presenter.greetingProvider = interactor
interactor.output = presenter
複製代碼

再來看一下特色評估:

  • 解耦(Distribution)——毋庸置疑,VIPER架構在職責間解耦的表現最好。
  • 可測試性(Testability)——不足爲奇,更好的解耦,更好的可測試性。
  • 易用性(Easy of use)——最後,上述兩個的表現所花費的代價你已經猜出來了。你不得不寫大量的沒有多少職責的接口(interface)類。

樂高積木提如今哪裏呢?

當使用VIPER時,感受就像用樂高積木搭建一座帝國大廈同樣,這是一個有問題的信號。也許,對於你的應用來講如今使用VIPER架構還爲時過早,你能夠考慮一個簡單的架構。有些人則選擇忽略這個問題,還繼續大炮打麻雀——大材小用。我猜想他們以爲將來他們的應用會所以而受益,儘管如今維護成本高的不合情理。若是你也這樣想的話,我建議你試一下Generamba——一個能夠生成VIPER架構的工具。儘管如此,對我我的來講,這樣就像在使用有自動瞄準系統的大炮同樣而不是簡單地投石機。


結論

咱們已經看過了幾種架構模式,我但願你們都能爲各自的困惑找到答案,毫無疑問你會意識到這篇文章並無提供什麼高招,因此,選擇架構模式的關鍵是根據具體的狀況進行權衡、取捨。

所以,在同一個應用中出現架構混合是很正常的一件事。例如:你一開始用的是MVC架構,忽然你意識到有一個特定的界面很難再用MVC架構進行有效的維護了,而後你就把它轉換成了MVVM架構並且僅僅只是對這一個界面進行了轉換。對於其餘的界面若是MVC架構工做正常的話沒有必要進行重構,由於這兩個架構很容易兼容。

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

儘量的簡化一切,但並不簡單——阿爾伯特·愛因斯坦

相關文章
相關標籤/搜索