作為iOS開發(fā)者應(yīng)該聽到過MVC埋虹,可能在考慮要不要轉(zhuǎn)到MVVM,或者聽說了VIPER這種高大上的,在想采用這種復(fù)雜的架構(gòu)值不值帕识?
本文試圖回答以上問題泛粹,幫助大家對iOS架構(gòu)有一個初步的了解。我們將通過一些簡單的例子介紹架構(gòu)的演進(jìn)肮疗,他們的異同晶姊。
為什么要考慮架構(gòu)選擇的問題
因?yàn)殚_發(fā)時如果不采用架構(gòu),隨著App復(fù)雜度的提高伪货,勢必出現(xiàn)一些巨大的類们衙,在其中定位及修復(fù)bug都會變得越來越困難。代碼組織很可能會是這樣的:
- 那些巨大的類是UIViewController的子類
- 在UIViewController中操作數(shù)據(jù)
- UIView基本不干啥
- Model只是些數(shù)據(jù)結(jié)構(gòu)碱呼,沒有動作
- 單元測試并沒有覆蓋到什么
讓人更加郁悶的是砍艾,你明明是按照Apple的推薦來組織代碼的,用的就是Apple的MVC架構(gòu)巍举。不奇怪脆荷,Apple的MVC是有問題的。
好的架構(gòu)需要什么特性
- 不同模塊角色明晰懊悯,代碼均衡分布于這些模塊上
- 由第1點(diǎn)帶來的可測試性
- 易用性蜓谋,維護(hù)成本低
隨著App復(fù)雜度的提高,終會達(dá)到大腦清晰思考的極限炭分。解決之道就是拆分成多個組件桃焕,遵循single responsibility principle,每個組件只完成一個功能捧毛,而這個功能完全封裝在這個組件中观堂。
可測試性對于已經(jīng)嘗到單元測試甜頭的開發(fā)者來說是理所當(dāng)然的,尤其是在增加新特性或者重構(gòu)之后發(fā)現(xiàn)測試通不過的時候呀忧。測試可以提前發(fā)現(xiàn)在運(yùn)行時會出現(xiàn)的問題师痕,設(shè)想這些問題會在用戶使用時發(fā)生,而修復(fù)要在審核周期后才能上線而账。
易用性很好理解胰坟。我們說寫的代碼越少,bug就越少泞辐。所以追求代碼量少笔横,絕不是因?yàn)殚_發(fā)者懶。同時也要避免那些會提高維護(hù)成本的奇技淫巧咐吼。
常見架構(gòu)
- MVC
- MVVM
- VIPER
前兩者結(jié)構(gòu)類似吹缔,都是把App的模塊分成3個大類:
Models:負(fù)責(zé)數(shù)據(jù)或者操作數(shù)據(jù)的數(shù)據(jù)存取層。例如User或者UserDataProvider類锯茄。
Views:負(fù)責(zé)展示層(GUI)厢塘。對iOS來說,包括所有那些前綴是UI的東西。
Controller / ViewModel:Model俗冻,View之間的膠水或者中間人礁叔。對用戶在View所做的操作進(jìn)行響應(yīng),改變Model迄薄,同時當(dāng)Model變化時琅关,更新View。
把模塊分開的好處:
- 更容易理解
- 利于重用讥蔽,尤其是View和Model
- 便于隔離開進(jìn)行測試
MVC
期望
Controller是溝通View和Model的中間人涣易,View和Model之間相互是不知道的。其中重用性最低的是Controller冶伞,這并不是個問題新症,畢竟,總要有個地方放置業(yè)務(wù)邏輯响禽。
這個結(jié)構(gòu)看上去很容易理解徒爹。但現(xiàn)實(shí)的情況是View Controller會變的非常臃腫,給View Controller減肥成為開發(fā)者的一個重要課題芋类。這是怎么發(fā)生的呢隆嗅?
現(xiàn)實(shí)
Cocoa MVC事實(shí)上鼓勵你寫臃腫的view controller,他們與view生命周期耦合的如此緊密侯繁,以致很難說他們是分開的胖喳。盡管你仍然可以把一些業(yè)務(wù)邏輯,數(shù)據(jù)轉(zhuǎn)化遷移到Model贮竟,想把一些工作轉(zhuǎn)移給view就沒那么容易了丽焊。大多數(shù)時間,所有view的工作就是發(fā)送動作給controller咕别。View controller最后成為各種代理和數(shù)據(jù)源的集中地技健,同時還要負(fù)責(zé)發(fā)起和取消網(wǎng)絡(luò)請求,等等顷级。
這是一段極其常見的代碼:
var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell
userCell.configureWithUser(user)
在這里凫乖,cell屬于view,但是直接由model來配置弓颈,MVC的原則被打破了,這種寫法太常見了删掀,人們都不覺得這里有什么問題翔冀。如果我們嚴(yán)格遵循MVC的原則,在controller中配置cell披泪,不把Model傳給view, 那就要往已經(jīng)臃腫的controller里塞進(jìn)更多代碼纤子。
如果不寫單元測試,這個問題可能還不那么明顯。由于controller與view的緊密耦合控硼,測試變的很困難泽论,因?yàn)椴坏貌灰獎?chuàng)造性的模擬view及其生命周期。
來看一個簡單的例子:
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: Selector(("didTapButton:")), for: .touchUpInside)
}
func didTapButton(button: UIButton) {
let greeting = "Hello \(self.person.firstName) \(self.person.lastName)"
self.greetingLabel.text = greeting
}
// layout code goes here
}
// Assembling of MVC
let model = Person(firstName: "John", lastName: "Smith")
let view = GreetingViewController()
view.person = model
這看上去不太好測試卡乾。我們可以把greeting的生成移到Model里翼悴,分開測試,而我們想要測試GreetingViewController中那些展示邏輯的話幔妨,就必須直接調(diào)用UIView相關(guān)的方法(viewDidLoad, didTapButton)鹦赎,這樣的話,就需要加載所有的view误堡,這對單元測試來說可不是好事古话。
View和controller之間的交互,恐怕就是無法用單元測試來測試锁施。
到這里陪踩,看上去Cocoa MVC是一個糟糕的設(shè)計(jì)模式。讓我們用前文提到的特性列表來評估一下:
- 分布:View和Model確實(shí)是分開的悉抵,但是view和controller是緊密耦合的肩狂。
- 可測試性:由于糟糕的分布,你可能只會測試Model基跑。
- 易用:在所有模式中代碼量是最少的婚温。另外,對所有人來說都很熟悉媳否,所以即使是生手也能維護(hù)栅螟。
如果你不打算花大量時間在架構(gòu)上,Cocoa MVC將是你的選擇篱竭,或者你覺得用一個較高維護(hù)成本的架構(gòu)開發(fā)一個小項(xiàng)目是殺雞用牛刀力图。
對于開發(fā)速度來說,Cocoa MVC是最好的設(shè)計(jì)模式。
MVVM
MVVM是MV(X)系列中最新的一種,希望之前MV(X)中存在的一些問題屹培,他都考慮進(jìn)去了比肄。
理論上Model-View-ViewModel看上去很不錯。View和Model我們已經(jīng)很熟悉了泪喊,還包括中間人,由View Model表示。
- MVVM把view controller視作View募舟。在這里,View Controller的子類實(shí)際上屬于View闻察,而不是中間人拱礁。
- View和Model之間沒有緊密耦合琢锋。
- 中間人在這里表達(dá)為View Model。
另外呢灶,在view和view model之間存在binding吴超。
那么在iOS現(xiàn)實(shí)中,view model是什么呢鸯乃?他基本上是view及其狀態(tài)的代表鲸阻,并且是與UIKit無關(guān)的。View Model調(diào)用Model的變化飒责,同時根據(jù)Model的更新赘娄,更新自己。由于view和view model之間存在binding宏蛉,view也會相應(yīng)更新遣臼。
Bindings
Mac OS直接就支持Binding,但是iOS并不支持拾并。當(dāng)然iOS支持KVO和notification揍堰,但是沒有binding方便。
假設(shè)不想自己實(shí)現(xiàn)嗅义,我們有兩個選項(xiàng):
基于KVO的binding庫屏歹,例如Swift Bond,RZDataBinding之碗。
Functional Reactive Programming框架蝙眶,例如ReactiveCocoa,RxSwift褪那。
事實(shí)上幽纷,現(xiàn)在當(dāng)談到MVVM時,總是和ReactiveCocoa等聯(lián)系在一起的博敬,反之亦然友浸。雖然可以通過簡單的binding搭建MVVM,用ReactiveCocoa等可以充分發(fā)揮MVVM偏窝。
在我們簡單的例子里收恢,F(xiàn)RP框架甚至KVO都不需要。我們將顯式的通過showGreeting
方法讓view model更新祭往,用一個簡單的屬性作為greetingDidChange
回調(diào)函數(shù)伦意。
import UIKit
struct Person { // Model
let firstName: String
let lastName: String
}
protocol GreetingViewModelProtocol: class {
var greeting: String? { get }
var greetingDidChange: ((GreetingViewModelProtocol) -> ())? { get set } // function to call when greeting did change
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: Selector(("showGreeting")), for: .touchUpInside)
}
// layout code goes here
}
// Assembling of MVVM
let model = Person(firstName: "John", lastName: "Smith")
let viewModel = GreetingViewModel(person: model)
let view = GreetingViewController()
view.viewModel = viewModel
我們再來評估一下特性:
分布:MVVM的view比MVP的view責(zé)任更多,他通過設(shè)置binding硼补,根據(jù)view model更新狀態(tài)默赂。
可測試性:view model對view一無所知,所以測試比較容易括勺。View或許也能測缆八,但是因?yàn)橐蕾囉赨IKit,可能就想跳過了
易用性:如果采用binding疾捍,MVVM的代碼量會大為減少
MVVM是很有吸引力的奈辰,他不僅保持了之前方案的優(yōu)點(diǎn),而且因?yàn)閎inding在view中的采用乱豆,view更新不需要額外的代碼奖恰。與此同時,測試也比較便利宛裕。
VIPER
最后是VIPER瑟啃,不屬于MV(X)家族。
現(xiàn)在揩尸,你一定同意其職責(zé)的細(xì)分很優(yōu)秀蛹屿。VIPER在職責(zé)的細(xì)分上更進(jìn)了一步,有了5層岩榆。
Interactor: 包含了與數(shù)據(jù)(Entity)或者網(wǎng)絡(luò)相關(guān)的業(yè)務(wù)邏輯错负,例如創(chuàng)建數(shù)據(jù)的新實(shí)例,從服務(wù)器獲取數(shù)據(jù)勇边。對于那些任務(wù)犹撒,一般會使用一些Service或者Manager,這些一般不被認(rèn)為是VIPER模塊的組成部分粒褒,而認(rèn)為是外部依賴识颊。
Presenter: 包含與UI相關(guān)(同時與UIKit無關(guān))的業(yè)務(wù)邏輯,會調(diào)用Interactor的方法奕坟。
Entities: 普通的數(shù)據(jù)對象祥款,但不是數(shù)據(jù)存取層,因?yàn)槟菍儆贗nteractor負(fù)責(zé)的执赡。
Router: 負(fù)責(zé)VIPER各模塊之間的轉(zhuǎn)移镰踏。
VIPER模塊既可以是單一屏幕,也可以是應(yīng)用的整個user story沙合,比方說用戶認(rèn)證奠伪,可以是一個屏幕也可以是多個相關(guān)的屏幕來實(shí)現(xiàn)。一塊積木有多大首懈,是由你決定的绊率。
通過與MV(X)類的比較,可以發(fā)現(xiàn)在責(zé)任的分布上是有些不同的:
Model(數(shù)據(jù)交互)邏輯移到了Interactor究履,而Entity只是啞的數(shù)據(jù)結(jié)構(gòu)滤否。
Controller/ViewModel中,只有UI表達(dá)的職責(zé)交給了Presenter最仑,而不包括改變數(shù)據(jù)的能力藐俺。
VIPER是第一個明確談到導(dǎo)航職責(zé)的模式炊甲,由Router來解決。
對于iOS應(yīng)用來說欲芹,實(shí)現(xiàn)合適的路徑導(dǎo)航是一個具有挑戰(zhàn)性的任務(wù)卿啡。MV(X)直接忽略了這個問題。
這個例子里面沒有包括導(dǎo)向和模塊之間交互的內(nèi)容菱父。
import UIKit
struct Person { // Entity (usually more complex e.g. NSManagedObject)
let firstName: String
let lastName: String
}
struct GreetingData { // Transport data structure (not Entity)
let greeting: String
let 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: "John", lastName: "Smith") // usually comes from data access layer
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: Selector(("didTapButton:")), for: .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 Router
let view = GreetingViewController()
let presenter = GreetingPresenter()
let interactor = GreetingInteractor()
view.eventHandler = presenter
presenter.view = view
presenter.greetingProvider = interactor
interactor.output = presenter
我們再來分析下特性:
- 分布:毫無疑問颈娜,VIPER是在職責(zé)分布上是做的最好的。
- 可測試性:更好的分布帶來了更好的可測試性浙宜。
- 易用性:最后官辽,和你猜的一樣,上述兩者需要付出的代價(jià)是可維護(hù)性粟瞬。你不得不為類寫大量的接口同仆,每個只負(fù)責(zé)很小的部分。
總結(jié)
我們介紹了幾種設(shè)計(jì)模式亩钟,希望能對你有所幫助乓梨。可能你也意識到了清酥,沒有銀彈扶镀,所以需要你在遇到實(shí)際問題時,權(quán)衡利弊焰轻,選擇架構(gòu)臭觉。
因此,可以在一些App中采用混合架構(gòu)辱志。舉個例子蝠筑,用MVC起步,然后發(fā)現(xiàn)采用MVC揩懒,有一個屏幕難以有效管理什乙,可以只針對這個屏幕切換到MVVM。沒有必要重構(gòu)其他MVC工作的好好的屏幕已球,這兩個架構(gòu)是很容易兼容的臣镣。
Everything should be made as simple as possible, but no simpler. ? Albert Einstein