爲避免撕逼,提早聲明:本文純屬翻譯,僅僅是爲了學習,加上水平有限,見諒!react
【原文】https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52ios
使用MVC
進行iOS
開發感受到很怪異?在切換到MVVM
的時候心存疑慮?據說過VIPER
,可是不知道是否值得采用? 讀下去,這篇文章將爲你一一解惑。 若是你正打算組織一下在iOS
環境下你掌握的架構模式知識體系。咱們接下來回簡單地回顧幾個流行的架構並作幾個小的練習。關於某個例子若是你想了解的更詳細一些,能夠查看下方的連接。git
掌握設計模式會讓人沉迷其中,全部必定要小心:相比閱讀本文章以前,你可能會問更多像這樣的問題: 由誰進行網絡請求:Model
?仍是ViewController
?github
如何向新視圖(View
)的ViewModel
中傳遞Model
web
由誰建立一個新的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
讓咱們定義一下一個好的架構應該有的特色:設計模式
在咱們試弄清楚事物是如何運做的時候,解耦能夠保證大腦的負載均衡。若是你認爲開發的(項目)越多你的大腦越能適應理解複雜的問題,那麼你就是對的。可是這個能力不是線性擴展的而且很快就能達到上限。因此,解決複雜性的最簡單的方式就是在多個實體間按照「單一責任原則」 拆分職責。promise
對於那些已經習慣了單元測試的人來講這並非一個問題,由於再添加了新的特性和重構了一個複雜的類後一般會運行失敗。這意味着單元測試能夠幫助開發者發現一些在運行時纔會出現的問題,而且這些問題常見於安裝在用戶的手機上的應用上,此外要修復這些問題也須要大概一週的時間。
這個問題並不須要回答,但,值得一提的是:最好的代碼老是那些沒有被寫出來的代碼。所以,代碼越少,錯誤也就越少。這也說明,總想着寫最少代碼的開發者不是由於他們懶,而且你也不該該由於一個更聰明的解決方案而忽視維護成本。
##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是什麼樣的。
View
)是無狀態的。一旦模型(
Model
)改變視圖(
View
)僅僅只是被控制器(
Controller
)渲染而已。想象一下點擊一個連接導航到其餘地方後網頁徹底加載出來的狀況。儘管,iOS應用可使用傳統的MVC架構,但這並無多大意義,由於架構自己就存在問題——三個實體(
entities
)之間聯繫太過緊密,每個實體都知道(引用)另外兩個實體。這就致使了實體的複用性急劇降低——在你的應用中這並非你所想要的。出於這個緣由,咱們就不寫MVC範例了。
Traditional MVC doesn't seems to be applicable to modern iOS development. 傳統MVC架構看上去並不適合用於如今的iOS開發中。
控制器(Controller
)是視圖(View
)與模型(Model
)二者之間的中介,這使得視圖(View
)與模型(Model
)都不知道對方的存在。控制器(Controller
)是可複用的最少的,對咱們來講這一般很好,由於咱們必須有一個地方去放置一些不適合放在模型(Model
)中且比較棘手的邏輯。 理論上,看起來很是簡單,可是你感受到有些地方不對,是否是這樣?你甚至據說過人們把MVC稱做Massive View Controller。此外,視圖控制器"瘦身"(View Controller offloading)成了iOS開發者中的一個重要話題。若是蘋果只是採用傳統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
多是一個至關糟糕的選擇。讓咱們按照文章開頭定義的特色來評估一下這種架構模式:
View
)和模型(Model
)確實解耦了,然而,視圖(View
)和控制器(Controller
)倒是緊密耦合的。Model
)。若是你沒有打算在架構時耗費太多時間而且以爲高成本的維護費用對你的小項目來講是一種過分的浪費的話,那麼Cocoa MVC
就是你的最好選擇。
Cocoa MVC is the best architectural pattern in term of the speed of the development. 在開發速度上面
Cocoa MVC
是最好的架構模式。
MVP
(Passive View variant)。等下...是否是
Apple’s MVC
事實上就是
MVP
?並非,回想一下在
MVC
中
View
和
Controller
是緊密耦合的,然而,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
的特色:
Presenter
)和模型(Model
),還有至關簡單、輕薄的視圖(dumb view)(在上述例子中的模型也很簡單)。MVP in iOS means superb testability and a lot of code
iOS
中的MVP
架構意味着極好的可測試性和大量的代碼。
####綁定和Hooters 還有一種類型的MVP
架構模式——the Supervising Controller MVP。這個變種包括了視圖和模型的直接綁定,展現器(The Supervising Controller)在處理動做的同時還能夠改變視圖。
可是,就如咱們已經知道的,模糊的職責拆分是不正確的,視圖和模型的緊耦合也一樣不可取。這和Cocoa
桌面應用開發很類似。和傳統的MVC
同樣,給有瑕疵的架構寫例子沒有任何意義。
MV(X)
類中近期最優秀的架構(The latest and the greatest of the MV(X) kind)MVVM是MV(X)這類中最新的架構形式,因此,咱們但願它可以解決MV(X)
以前所面臨的問題。
理論上,Model-View_ViewModel
這種架構很棒。不只View
和Model
,並且Mediator
——至關於View Model
,咱們都已經熟悉。
MVP
很類似:
View
)和模型(Model
)之間不存在緊耦合。另外,它還能夠像MVP
那樣綁定;可是綁定不是發生在視圖(View
)和模型(Model
)之間,而是視圖(View
)和視圖模型(View Model
)之間。
那麼,iOS現實中的視圖模型(View Model
)的廬山面目是什麼?它是你的視圖及其狀態的基本的UIKit
的獨立展現。視圖模型觸發模型的改變,並利用改變後的Model
更新本身,因爲咱們在視圖和視圖模型之間進行了綁定,視圖也會根據視圖模型的改變而改變。
綁定我在講解MVP
架構部分簡單的提到過,這裏咱們在對其進行一些討論。綁定是從OSX
開發而來的,並且iOS中並無這個概念。固然,咱們有KVO
和通知(notifications
),可是它的使用並無綁定方便。
因此,假若不想本身編寫綁定代碼,咱們還有兩個選擇:
RZDataBinding
或者SwiftBond
。ReactiveCocoa
、RxSwift
或者PromiseKit
`。事實上,現今,只要你聽到「MVVM
」你就會想到ReactiveCocoa
,反之亦然。儘管使用簡單地綁定也能夠建立MVVM
架構的項目,可是,ReactiveCocoa
(或者同類的庫)卻可讓你把使用MVVM
架構的優點最大化。
關於Reactive
庫有一個殘酷的現實須要面對:功能強大卻伴隨着巨大的職責。當使用Reactive
庫的時候極容易把不少事情搞混,若是出現錯誤,你可能須要花費不少的時間去在APP中定位問題所在,因此看一下下圖的調用堆棧。
在簡單的例子中,使用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
複製代碼
再回過來看一下咱們的特色評估:
MVP
的視圖擁有更多的職責。由於,前者經過綁定從視圖模型(ViewModel
)更新本身,然後者則是把全部的事件前置到Presenter
中,也不對本身的狀態進行更新。View Model
對View
一無所知,這可讓咱們輕易地對其進行測試。也能夠對視圖(View
)測試,但因爲UIKit
依賴,你可能想跳過她。MVVM
同MVP
有一樣的代碼量,可是在實際的應用中,對於MVP
你須要把全部事件從視圖(View
)前置到展現器(Presenter
)並手動的更新視圖,而對於MVVM
,若是你使用了綁定則會變的很容易。MVVM極其吸人眼球,它融合了上述全部架構的的優點,此外,因爲它在視圖(
View
)端進行了綁定,你能夠不須要任何額外的代碼對視圖(View
)進行更新。雖然如此,可測試性依然保持在一個很好的層次。
###VIPER ####把搭建樂高積木的經驗拿到iOS應用設計中使用 VIPER
使咱們最後的選擇,這種架構尤其有趣,由於他不是屬於MV(X)類的架構。
到目前爲止,關於職責粒度的劃分很是合理這點你確定贊同。VIPER
在職責劃分上面又作了一次迭代,此次咱們一共有五層。
Services
和Managers
,這些不能算是VIPER的一部分,更確切的說只是些外部依賴。Interactor
)中的方法。Interactor
)的職責。VIPER
中的模塊。大體上說,VIPER
模塊能夠是一個界面,也能夠是整個應用的用戶界面(user story)——想象一下驗證功能,它能夠是一個界面也能夠是幾個相關聯的界面。」樂高積木「塊應該多大呢?——這取決你本身。
若是咱們同MV(X)
這一類進行比較,咱們能夠看到幾個不一樣的職責解耦之處:
Model
)(數據交互(data interaction))邏輯轉移到了交互器(Interactor
)中,同時**實體(Entities
)**做爲單一的數據結構存在。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
複製代碼
再來看一下特色評估:
當使用VIPER
時,感受就像用樂高積木搭建一座帝國大廈同樣,這是一個有問題的信號。也許,對於你的應用來講如今使用VIPER
架構還爲時過早,你能夠考慮一個簡單的架構。有些人則選擇忽略這個問題,還繼續大炮打麻雀——大材小用。我猜想他們以爲將來他們的應用會所以而受益,儘管如今維護成本高的不合情理。若是你也這樣想的話,我建議你試一下Generamba——一個能夠生成VIPER
架構的工具。儘管如此,對我我的來講,這樣就像在使用有自動瞄準系統的大炮同樣而不是簡單地投石機。
咱們已經看過了幾種架構模式,我但願你們都能爲各自的困惑找到答案,毫無疑問你會意識到這篇文章並無提供什麼高招,因此,選擇架構模式的關鍵是根據具體的狀況進行權衡、取捨。
所以,在同一個應用中出現架構混合是很正常的一件事。例如:你一開始用的是MVC
架構,忽然你意識到有一個特定的界面很難再用MVC
架構進行有效的維護了,而後你就把它轉換成了MVVM
架構並且僅僅只是對這一個界面進行了轉換。對於其餘的界面若是MVC
架構工做正常的話沒有必要進行重構,由於這兩個架構很容易兼容。
Make everything as simple as possible, but not simpler——Albert Einstein
儘量的簡化一切,但並不簡單——阿爾伯特·愛因斯坦