為避免撕逼,提前宣告:本文純屬翻譯,僅僅是為了學習,加上水平有限,見諒!
【原文】https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52
使用MVC
進行iOS
開發感覺到很怪異?在切換到MVVM
的時候心存疑慮?聽說過VIPER
,但是不知道是否值得采用?
讀下去,這篇文章將為你一一解惑。
如果你正打算組織一下在iOS
環境下你掌握的架構模式知識體系。我們接下來回簡單地回顧幾個流行的架構並做幾個小的練習。關於某個例子如果你想了解的更詳細一些,可以檢視下方的連結。
掌握設計模式會讓人沉迷其中,所有一定要當心:相比閱讀本文章之前,你可能會問更多像這樣的問題:
由誰進行網路請求:Model
?還是ViewController
?
如何向新檢視(View
)的ViewModel
中傳遞Model
由誰建立一個新的VIPER
模組:路由(Router
)?還是展示器(Presenter
)?
為什麼在乎架構的選擇?
因為如果你不這樣做,終有一天,你在除錯一個擁有著數十個不同方法和變數(things)的龐大的類檔案時,你會發現你無法找到並修復此檔案中的任何問題。自然地,也很難把這個類檔案當做一個整體而熟稔於心,這樣你可能總是會錯過一些重要的細節。如果你的應用已經處於這樣的境況,很有可能是這樣:
- 這個類是
UIViewController
的子類。 - 你的資料直接儲存在
UIViewController
中。 - 你的
UIViews
什麼都不做。 Model
是啞資料結構(dumb data structure
)。
dumb data structure: 只用來儲存資料的結構,沒有任何方法。詳見:https://stackoverflow.com/questions/32944751/what-is-dumb-data-and-dumb-data-object-mean
- 單元測試沒有0覆蓋。 即使你是按照蘋果的指導方針並實現蘋果的MVC模式,也會出現上述問題,所有不要難過。蘋果的MVC模式存在著一些些問題,這點我們稍後再說。
讓我們定義一下一個好的架構應該有的特點:
- 能把程式碼職責均衡的解耦到不同的功能類裡。(Balanced distribution of responsibilities among entities with strict roles.)
- 可測試性(Testability usually comes from the first feature.)。
- 易用、維護成本低(Ease of use and a low maintenance cost.)。
解耦( Why Distribution ?)
在我們試弄清楚事物是如何運作的時候,解耦可以保證大腦的負載均衡。如果你認為開發的(專案)越多你的大腦越能適應理解複雜的問題,那麼你就是對的。但是這個能力不是線性擴充套件的並且很快就能達到上限。所以,解決複雜性的最簡單的方式就是在多個實體間按照“單一責任原則” 拆分職責。
可測試(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是什麼樣的。
在這種情況中,檢視(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
預期
控制器(Controller
)是檢視(View
)與模型(Model
)兩者之間的中介,這使得檢視(View
)與模型(Model
)都不知道對方的存在。控制器(Controller
)是可複用的最少的,對我們來說這通常很好,因為我們必須有一個地方去放置一些不適合放在模型(Model
)中且比較棘手的邏輯。
理論上,看起來非常簡單,但是你感覺到有些地方不對,是不是這樣?你甚至聽說過人們把MVC稱作Massive View Controller。此外,檢視控制器"瘦身"(View Controller offloading)成了iOS開發者中的一個重要話題。如果蘋果只是採用傳統MVC
架構或者只是稍加改進,為什麼會出現這種情況?
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
是不是很像蘋果的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
的特點:
- 解耦(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)在處理動作的同時還可以改變檢視。
但是,就如我們已經知道的,模糊的職責拆分是不正確的,檢視和模型的緊耦合也同樣不可取。這和Cocoa
桌面應用開發很相似。和傳統的MVC
一樣,給有瑕疵的架構寫例子沒有任何意義。
MVVM
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
很相似:
- MVVM把檢視控制器當做檢視。
- 檢視(
View
)和模型(Model
)之間不存在緊耦合。
另外,它還可以像MVP
那樣繫結;但是繫結不是發生在檢視(View
)和模型(Model
)之間,而是檢視(View
)和檢視模型(View Model
)之間。
那麼,iOS現實中的檢視模型(View Model
)的廬山面目是什麼?它是你的檢視及其狀態的基本的UIKit
的獨立展示。檢視模型觸發模型的改變,並利用改變後的Model
更新自己,由於我們在檢視和檢視模型之間進行了繫結,檢視也會根據檢視模型的改變而改變。
繫結(Bindings)
繫結我在講解MVP
架構部分簡單的提到過,這裡我們在對其進行一些討論。繫結是從OSX
開發而來的,而且iOS中並沒有這個概念。當然,我們有KVO
和通知(notifications
),但是它的使用並沒有繫結方便。
所以,倘若不想自己編寫繫結程式碼,我們還有兩個選擇:
- 一個是,基於KVO的繫結庫,像
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
複製程式碼
再回過來看一下我們的特點評估:
- 解耦(Distribution)——在我們上面的的簡例中可能不太明顯,事實上,MVVM的檢視比
MVP
的檢視擁有更多的職責。因為,前者通過繫結從檢視模型(ViewModel
)更新自己,而後者則是把所有的事件前置到Presenter
中,也不對自己的狀態進行更新。 - 可測試性(Testability)——
View Model
對View
一無所知,這可以讓我們輕易地對其進行測試。也可以對檢視(View
)測試,但由於UIKit
依賴,你可能想跳過她。 - 易用性(Easy of use)——在我們的例子中,
MVVM
同MVP
有同樣的程式碼量,但是在實際的應用中,對於MVP
你需要把所有事件從檢視(View
)前置到展示器(Presenter
)並手動的更新檢視,而對於MVVM
,如果你使用了繫結則會變的很容易。
MVVM極其吸人眼球,它融合了上述所有架構的的優勢,此外,由於它在檢視(
View
)端進行了繫結,你可以不需要任何額外的程式碼對檢視(View
)進行更新。雖然如此,可測試性依然保持在一個很好的層次。
###VIPER
####把搭建樂高積木的經驗拿到iOS應用設計中使用
VIPER
使我們最後的選擇,這種架構尤為有趣,因為他不是屬於MV(X)類的架構。
到目前為止,關於職責粒度的劃分非常合理這點你肯定贊同。VIPER
在職責劃分上面又做了一次迭代,這次我們一共有五層。
- 互動器(Interactor)——包含與資料(Entities)或者網路相關的業務邏輯,向建立一個實體的物件或者從網路獲取物件。為了這個目的,你需要用到一些
Services
和Managers
,這些不能算是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
儘可能的簡化一切,但並不簡單——阿爾伯特·愛因斯坦