Make everything as simple as possible, but not simpler?—?Albert Einstein
把每件事娘摔,做簡(jiǎn)單到極致,但又不過于簡(jiǎn)單 - 阿爾伯特·愛因斯坦
在使用 iOS 的 MVC 時(shí)候感覺怪怪的唤反?想要嘗試下 MVVM凳寺?之前聽說過 VIPER,但是又糾結(jié)是不是值得去學(xué)彤侍?
繼續(xù)閱讀肠缨,你就會(huì)知道上面問題的答案 - 如果讀完了還是不知道的話,歡迎留言評(píng)論盏阶。
iOS 上面的架構(gòu)模式你可能之前就了解過一些晒奕,接下來我們會(huì)幫你把它們進(jìn)行一下梳理。我們先簡(jiǎn)要回顧一下目前比較主流的架構(gòu)模式名斟,分析比較一些他們的原理吴汪,并用一些小栗子來進(jìn)行練習(xí)。如果你對(duì)其中的某一種比較感興趣的話蒸眠,我們也在文章里面給出了對(duì)應(yīng)的鏈接漾橙。
對(duì)于設(shè)計(jì)模式的學(xué)習(xí)是一件容易上癮的事情,所以先提醒你一下:在你讀完這篇文章之后楞卡,可能會(huì)比讀之前有更多的疑問霜运,比如:
(MVC)誰來負(fù)責(zé)網(wǎng)絡(luò)請(qǐng)求:是 Model 還是 Controller脾歇?
(MVVM)我該怎么去把一個(gè) Model 傳遞給一個(gè)新創(chuàng)建的 View 的 ViewModel?
(VIPER)誰來負(fù)責(zé)創(chuàng)建 VIPER 模塊:是 Router 還是 Presenter淘捡?
為何要在意架構(gòu)的選擇呢藕各?
因?yàn)槿绻悴辉谝獾脑挘y保一天焦除,你就需要去調(diào)試一個(gè)巨大無比又有著各種問題的類激况,然后你會(huì)發(fā)現(xiàn)在這個(gè)類里面,你完全就找不到也修復(fù)不了任何 bug膘魄。一般來說乌逐,把這么大的一個(gè)類作為整體放在腦子里記著是一件非常困難的事情,你總是難免會(huì)忘掉一些比較重要的細(xì)節(jié)创葡。如果你發(fā)現(xiàn)在你的應(yīng)用里面已經(jīng)開始出現(xiàn)這種狀況了浙踢,那你很可能遇到過下面這類問題:
- 這個(gè)類是一個(gè) UIViewController 的子類。
- 你的數(shù)據(jù)直接保存在了 UIViewController 里面灿渴。
- 你的 UIViews 好像什么都沒做洛波。
- 你的 Model 只是一個(gè)純粹的數(shù)據(jù)結(jié)構(gòu)
- 你的單元測(cè)試什么都沒有覆蓋到
其實(shí)即便你遵循了 Apple 的設(shè)計(jì)規(guī)范,實(shí)現(xiàn)了 Apple 的 MVC 框架骚露,也還是一樣會(huì)遇到上面這些問題蹬挤;所以也沒什么好失落的。Apple 的 MVC 框架 有它自身的缺陷棘幸,不過這個(gè)我們后面再說闻伶。
讓我們先來定義一下好的框架應(yīng)該具有的特征:
- 用嚴(yán)格定義的角色,平衡的將職責(zé) 劃分 給不同的實(shí)體够话。
- 可測(cè)性 通常取決于上面說的第一點(diǎn)(不用太擔(dān)心,如果架構(gòu)何時(shí)的話光绕,做到這點(diǎn)并不難)女嘲。
- 易用 并且維護(hù)成本低。
為什么要?jiǎng)澐郑?/h3>
當(dāng)我們?cè)噲D去理解事物的工作原理的時(shí)候诞帐,劃分可以減輕我們的腦部壓力欣尼。如果你覺得開發(fā)的越多,大腦就越能適應(yīng)去處理復(fù)雜的工作停蕉,確實(shí)是這樣愕鼓。但是大腦的這種能力不是線性提高的,而且很快就會(huì)達(dá)到一個(gè)瓶頸慧起。所以要處理復(fù)雜的事情菇晃,最好的辦法還是在遵循 單一責(zé)任原則 的條件下,將它的職責(zé)劃分到多個(gè)實(shí)體中去蚓挤。
為什么要可測(cè)性磺送?
對(duì)于那些對(duì)單元測(cè)試心存感激的人來說驻子,應(yīng)該不會(huì)有這方面的疑問:?jiǎn)卧獪y(cè)試幫助他們測(cè)試出了新功能里面的錯(cuò)誤,或者是幫他們找出了重構(gòu)的一個(gè)復(fù)雜類里面的 bug估灿。這意味著這些單元測(cè)試幫助這些開發(fā)者們?cè)诔绦蜻\(yùn)行之前就發(fā)現(xiàn)了問題崇呵,這些問題如果被忽視的話很可能會(huì)提交到用戶的設(shè)備上去;而修復(fù)這些問題馅袁,又至少需要一周左右的時(shí)間(AppStore 審核)域慷。
為什么要易用
這塊沒什么好說的,直說一點(diǎn):最好的代碼是那些從未被寫出來的代碼汗销。代碼寫的越少犹褒,問題就越少;所以開發(fā)者想少寫點(diǎn)代碼并不一定就是因?yàn)樗麘写罅铩_€有化漆,當(dāng)你想用一個(gè)比較 聰明 的方法的時(shí)候,全完不要忽略了它的維護(hù)成本钦奋。
MV(X) 的基本要素
現(xiàn)在我們面對(duì)架構(gòu)設(shè)計(jì)模式的時(shí)候有了很多選擇:
首先前三種模式都是把所有的實(shí)體歸類到了下面三種分類中的一種:
- Models(模型):數(shù)據(jù)層座云,或者負(fù)責(zé)處理數(shù)據(jù)的數(shù)據(jù)接口層。比如 Person 和 PersonDataProvider 類
- Views(視圖):展示層(GUI)付材。對(duì)于 iOS 來說所有以 UI 開頭的類基本都屬于這層朦拖。
- Controller/Presenter/ViewModel(控制器/展示器/視圖模型):它是 Model 和 View 之間的膠水或者說是中間人。一般來說厌衔,當(dāng)用戶對(duì) View 有操作時(shí)它負(fù)責(zé)去修改相應(yīng) Model璧帝;當(dāng) Model 的值發(fā)生變化時(shí)它負(fù)責(zé)去更新對(duì)應(yīng) View。
將實(shí)體進(jìn)行分類之后我們可以:
- 更好的理解
- 重用(主要是 View 和 Model)
- 對(duì)它們獨(dú)立的進(jìn)行測(cè)試
讓我從 MV(X) 系列開始講起富寿,最后講 VIPER睬隶。
MVC - 它原來的樣子
在開始討論 Apple 的 MVC 之前,我們先來看下傳統(tǒng)的MVC页徐。
在這種架構(gòu)下苏潜,View 是無狀態(tài)的,在 Model 變化的時(shí)候它只是簡(jiǎn)單的被 Controller 重繪变勇;就像網(wǎng)頁一樣恤左,點(diǎn)擊了一個(gè)新的鏈接,整個(gè)網(wǎng)頁就重新加載搀绣。盡管這種架構(gòu)可以在 iOS 應(yīng)用里面實(shí)現(xiàn)飞袋,但是由于 MVC 的三種實(shí)體被緊密耦合著,每一種實(shí)體都和其他兩種有著聯(lián)系链患,所以即便是實(shí)現(xiàn)了也沒有什么意義巧鸭。這種緊耦合還戲劇性的減少了它們被重用的可能,這恐怕不是你想要在自己的應(yīng)用里面看到的麻捻。綜上蹄皱,傳統(tǒng) MVC 的例子我覺得也沒有必要去寫了览闰。
傳統(tǒng)的 MVC 已經(jīng)不適合當(dāng)下的 iOS 開發(fā)了。
Apple 的 MVC
理想
View 和 Model 之間是相互獨(dú)立的巷折,它們只通過 Controller 來相互聯(lián)系压鉴。有點(diǎn)惱人的是 Controller 是重用性最差的,因?yàn)槲覀円话悴粫?huì)把冗雜的業(yè)務(wù)邏輯放在 Model 里面锻拘,那就只能放在 Controller 里了油吭。
理論上看這么做貌似挺簡(jiǎn)單的,但是你有沒有覺得有點(diǎn)不對(duì)勁署拟?你甚至聽過有人把 MVC 叫做重控制器模式婉宰。另外關(guān)于 ViewController 瘦身 已經(jīng)成為 iOS 開發(fā)者們熱議的話題了。為什么 Apple 要沿用只是做了一點(diǎn)點(diǎn)改進(jìn)的傳統(tǒng) MVC 架構(gòu)呢推穷?
現(xiàn)實(shí)
Cocoa MVC 鼓勵(lì)你去寫重控制器是因?yàn)?View 的整個(gè)生命周期都需要它去管理心包,Controller 和 View 很難做到相互獨(dú)立。雖然你可以把控制器里的一些業(yè)務(wù)邏輯和數(shù)據(jù)轉(zhuǎn)換的工作交給 Model馒铃,但是你再想把負(fù)擔(dān)往 View 里面分?jǐn)偟臅r(shí)候就沒辦法了蟹腾;因?yàn)?View 的主要職責(zé)就只是講用戶的操作行為交給 Controller 去處理而已。于是 ViewController 最終就變成了所有東西的代理和數(shù)據(jù)源区宇,甚至還負(fù)責(zé)網(wǎng)絡(luò)請(qǐng)求的發(fā)起和取消娃殖,還有...剩下的你來講。
像下面這種代碼你應(yīng)該不陌生吧:
var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell
userCell.configureWithUser(user)
Cell 作為一個(gè) View 直接用 Model 來完成了自身的配置议谷,MVC 的原則被打破了炉爆,這種情況一直存在,而且還沒人覺得有什么問題卧晓。如果你是嚴(yán)格遵循 MVC 的話芬首,你應(yīng)該是在 ViewController 里面去配置 Cell,而不是直接將 Model 丟給 Cell逼裆,當(dāng)然這樣會(huì)讓你的 ViewController 更重郁稍。
Cocoa MVC 被戲稱為重控制器模式還是有原因的。
問題直到開始單元測(cè)試(希望你的項(xiàng)目里面已經(jīng)有了)之后才開始顯現(xiàn)出來波附。Controller 測(cè)試起來很困難,因?yàn)樗?View 耦合的太厲害昼钻,要測(cè)試它的話就需要頻繁的去 mock View 和 View 的生命周期掸屡;而且按照這種架構(gòu)去寫控制器代碼的話,業(yè)務(wù)邏輯的代碼也會(huì)因?yàn)橐晥D布局代碼的原因而變得很散亂然评。
我們來看下面這段 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:", forControlEvents: .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: "David", lastName: "Blaine")
let view = GreetingViewController()
view.person = model;
MVC 的組裝仅财,可以放在當(dāng)前正在顯示的 ViewController 里面
這段代碼看起來不太好測(cè)試對(duì)吧?我們可以把 greeting
的生成方法放到一個(gè)新類 GreetingModel
里面去單獨(dú)測(cè)試碗淌。但是我們?nèi)绻徽{(diào)用與 View 相關(guān)的方法的話 (viewDidLoad, didTapButton
)盏求,就測(cè)試不到 GreetingViewController
里面任何的顯示邏輯(雖然在上面這個(gè)例子里面抖锥,邏輯已經(jīng)很少了);而調(diào)用的話就可能需要把所有的 View 都加載出來碎罚,這對(duì)單元測(cè)試來說太不利了磅废。
實(shí)際上,在模擬器(比如 iPhone 4S)上運(yùn)行并測(cè)試 View 的顯示并不能保證在其他設(shè)備上(比如 iPad)也能良好運(yùn)行荆烈。所以我建議把「Host Application」從你的單元測(cè)試配置項(xiàng)里移除掉拯勉,然后在不啟動(dòng)模擬器的情況下去跑你的單元測(cè)試。
View 和 Controller 之間的交互憔购,并不能真正的被單元測(cè)試覆蓋宫峦。
補(bǔ)充:What's Worth Unit Testing in Objective-C ?
綜上所述,Cocoa MVC 貌似并不是一個(gè)很好的選擇玫鸟。但是我們還是評(píng)估一下他在各方面的表現(xiàn)(在文章開頭有講):
- 劃分 - View 和 Model 確實(shí)是實(shí)現(xiàn)了分離导绷,但是 View 和 Controller 耦合的太厲害
- 可測(cè)性 - 因?yàn)閯澐值牟粔蚯宄阅軠y(cè)的基本就只有 Model 而已
-
易用 - 相較于其他模式屎飘,它的代碼量最少妥曲。而且基本上每個(gè)人都很熟悉它,即便是沒太多經(jīng)驗(yàn)的開發(fā)者也能維護(hù)枚碗。
在這種情況下你可以選擇 Cocoa MVC:你并不想在架構(gòu)上花費(fèi)太多的時(shí)間逾一,而且你覺得對(duì)于你的小項(xiàng)目來說,花費(fèi)更高的維護(hù)成本只是浪費(fèi)而已肮雨。
如果你最看重的是開發(fā)速度遵堵,那么 Cocoa MVC 就是你最好的選擇。
MVP - 保證了職責(zé)劃分的(promises delivered) Cocoa MVC
看起來確實(shí)很像 Apple 的 MVC 對(duì)吧怨规?確實(shí)蠻像陌宿,它的名字是 MVP(被動(dòng)變化的 View)。稍等...這個(gè)意思是說 Apple 的 MVC 實(shí)際上是 MVP 嗎波丰?不是的壳坪,回想一下,在 MVC 里面 View 和 Controller 是耦合緊密的掰烟,但是對(duì)于 MVP 里面的 Presenter 來講爽蝴,它完全不關(guān)注 ViewController 的生命周期,而且 View 也能被簡(jiǎn)單 mock 出來纫骑,所以在 Presenter 里面基本沒什么布局相關(guān)的代碼蝎亚,它的職責(zé)只是通過數(shù)據(jù)和狀態(tài)更新 View。
如果我跟你講 UIViewController 在這里的角色其實(shí)是 View 你感覺如何先馆。
在 MVP 架構(gòu)里面耻陕,UIViewController 的那些子類其實(shí)是屬于 View 的吁津,而不是 Presenter簿姨。這種區(qū)別提供了極好的可測(cè)性,但是這是用開發(fā)速度的代價(jià)換來的宪拥,因?yàn)槟惚仨氁謩?dòng)的去創(chuàng)建數(shù)據(jù)和綁定事件,像下面這段代碼中做的一樣:
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: 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 MVP
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
let presenter = GreetingPresenter(view: view, person: model)
view.presenter = presenter
關(guān)于組裝方面的重要說明
MVP 架構(gòu)擁有三個(gè)真正獨(dú)立的分層铣减,所以在組裝的時(shí)候會(huì)有一些問題她君,而 MVP 也成了第一個(gè)披露了這種問題的架構(gòu)。因?yàn)槲覀儾幌胱?View 知道 Model 的信息徙歼,所以在當(dāng)前的 ViewController(角色其實(shí)是 View)里面去進(jìn)行組裝肯定是不正確的犁河,我們應(yīng)該在另外的地方完成組裝。比如魄梯,我們可以創(chuàng)建一個(gè)應(yīng)用層(app-wide)的 Router 服務(wù)桨螺,讓它來負(fù)責(zé)組裝和 View-to-View 的轉(zhuǎn)場(chǎng)。這個(gè)問題不僅在 MVP 中存在酿秸,在接下來要介紹的模式里面也都有這個(gè)問題灭翔。
讓我們來看一下 MVP 在各方面的表現(xiàn):
- 劃分 - 我們把大部分的職責(zé)都分配到了 Presenter 和 Model 里面,而 View 基本上不需要做什么(在上面的例子里面辣苏,Model 也什么都沒做)肝箱。
- 可測(cè)性 - 簡(jiǎn)直棒,我們可以通過 View 來測(cè)試大部分的業(yè)務(wù)邏輯稀蟋。
- 易用 - 就我們上面那個(gè)簡(jiǎn)單的例子來講煌张,代碼量差不多是 MVC 架構(gòu)的兩倍,但是 MVP 的思路還是蠻清晰的退客。
MVP 架構(gòu)在 iOS 中意味著極好的可測(cè)性和巨大的代碼量骏融。
MVP - 添加了數(shù)據(jù)綁定的另一個(gè)版本
還存在著另一種的 MVP - Supervising Controller MVP。這個(gè)版本的 MVP 包括了 View 和 Model 的直接綁定萌狂,與此同時(shí) Presenter(Supervising Controller)仍然繼續(xù)處理 View 上的用戶操作档玻,控制 View 的顯示變化。
但是我們之前講過茫藏,模糊的職責(zé)劃分是不好的事情误趴,比如 View 和 Model 的緊耦合。這個(gè)道理在 Cocoa 桌面應(yīng)用開發(fā)上面也是一樣的务傲。
就像傳統(tǒng) MVC 架構(gòu)一樣凉当,我找不到有什么理由需要為這個(gè)有瑕疵的架構(gòu)寫一個(gè)例子。
MVVM - 是 MV(X) 系列架構(gòu)里面最新興的售葡,也是最出色的
MVVM 架構(gòu)是 MV(X) 里面最新的一個(gè)看杭,讓我們希望它在出現(xiàn)的時(shí)候已經(jīng)考慮到了 MV(X) 模式之前所遇到的問題吧。
理論上來說天通,Model - View - ViewModel 看起來非常棒泊窘。View 和 Model 我們已經(jīng)都熟悉了熄驼,中間人的角色我們也熟悉了像寒,但是在這里中間人的角色變成了 ViewModel烘豹。
它跟 MVP 很像:
- MVVM 架構(gòu)把 ViewController 看做 View。
- View 和 Model 之間沒有緊耦合
另外诺祸,它還像 Supervising 版的 MVP 那樣做了數(shù)據(jù)綁定携悯,不過這次不是綁定 View 和 Model,而是綁定 View 和 ViewModel筷笨。
那么憔鬼,iOS 里面的 ViewModel 到底是個(gè)什么東西呢?本質(zhì)上來講胃夏,他是獨(dú)立于 UIKit 的轴或, View 和 View 的狀態(tài)的一個(gè)呈現(xiàn)(representation)。ViewModel 能主動(dòng)調(diào)用對(duì) Model 做更改仰禀,也能在 Model 更新的時(shí)候?qū)ψ陨磉M(jìn)行調(diào)整照雁,然后通過 View 和 ViewModel 之間的綁定,對(duì) View 也進(jìn)行對(duì)應(yīng)的更新答恶。
綁定
我在 MVP 的部分簡(jiǎn)單的提過這個(gè)內(nèi)容饺蚊,在這里讓我們?cè)傺由煊懻撘幌隆=壎ㄟ@個(gè)概念源于 OS X 平臺(tái)的開發(fā)悬嗓,但是在 iOS 平臺(tái)上面污呼,我們并沒有對(duì)應(yīng)的開發(fā)工具。當(dāng)然包竹,我們也有 KVO 和 通知燕酷,但是用這些方式去做綁定不太方便。
那么映企,如果我們不想自己去寫他們的話悟狱,下面提供了兩個(gè)選擇:
- 選一個(gè)基于 KVO 的綁定庫(kù),比如 RZDataBinding 或者 SwiftBond堰氓。
- 使用全量級(jí)的 函數(shù)式響應(yīng)編程 框架,比如 ReactiveCocoa挤渐、RxSwift 或者 PromiseKit。
實(shí)際上双絮,現(xiàn)在提到「MVVM」你應(yīng)該就會(huì)想到 ReactiveCocoa浴麻,反過來也是一樣。雖然我們可以通過簡(jiǎn)單的綁定來實(shí)現(xiàn) MVVM 模式囤攀,但是 ReactiveCocoa(或者同類型的框架)會(huì)讓你更大限度的去理解 MVVM软免。
響應(yīng)式編程框架也有一點(diǎn)不好的地方,能力越大責(zé)任越大嘛焚挠。用響應(yīng)式編程用得不好的話膏萧,很容易會(huì)把事情搞得一團(tuán)糟。或者這么說榛泛,如果有什么地方出錯(cuò)了蝌蹂,你需要花費(fèi)更多的時(shí)間去調(diào)試〔芟牵看著下面這張調(diào)用堆棧圖感受一下:
在接下來的這個(gè)小例子中孤个,用響應(yīng)式框架(FRF)或者 KVO 都顯得有點(diǎn)大刀小用,所以我們用另一種方式:直接的調(diào)用 ViewModel 的 showGreeting
方法去更新自己(的 greeting
屬性)沛简,(在 greeting
屬性的 didSet
回調(diào)里面)用 greetingDidChange
閉包函數(shù)去更新 View 的顯示齐鲤。
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: "showGreeting", forControlEvents: .TouchUpInside)
}
// layout code goes here
}
// Assembling of MVVM
let model = Person(firstName: "David", lastName: "Blaine")
let viewModel = GreetingViewModel(person: model)
let view = GreetingViewController()
view.viewModel = viewModel
然后,我們?cè)倩剡^頭來對(duì)它各方面的表現(xiàn)做一個(gè)評(píng)價(jià):
- 劃分 - 這在我們的小栗子里面表現(xiàn)的不是很清楚椒楣,但是 MVVM 框架里面的 View 比 MVP 里面負(fù)責(zé)的事情要更多一些给郊。因?yàn)榍罢呤峭ㄟ^ ViewModel 的數(shù)據(jù)綁定來更新自身狀態(tài)的,而后者只是把所有的事件統(tǒng)統(tǒng)交給 Presenter 去處理就完了捧灰,自己本身并不負(fù)責(zé)更新丑罪。
- 可測(cè)性 - 因?yàn)?ViewModel 對(duì) View 是一無所知的,這樣我們對(duì)它的測(cè)試就變得很簡(jiǎn)單凤壁。View 應(yīng)該也是能夠被測(cè)試的吩屹,但是可能因?yàn)樗鼘?duì) UIKit 的依賴,你會(huì)直接略過它拧抖。
-
易用 - 在我們的例子里面煤搜,它的代碼量基本跟 MVP 持平,但是在實(shí)際的應(yīng)用當(dāng)中 MVVM 會(huì)更簡(jiǎn)潔一些唧席。因?yàn)樵?MVP 下你必須要把 View 的所有事件都交給 Presenter 去處理擦盾,而且需要手動(dòng)的去更新 View 的狀態(tài);而在 MVVM 下淌哟,你只需要用綁定就可以解決迹卢。
MVVM 真的很有魅力,因?yàn)樗粌H結(jié)合了上述幾種框架的優(yōu)點(diǎn)徒仓,還不需要你為視圖的更新去寫額外的代碼(因?yàn)樵?View 上已經(jīng)做了數(shù)據(jù)綁定)腐碱,另外它在可測(cè)性上的表現(xiàn)也依然很棒。
VIPER - 把搭建樂高積木的經(jīng)驗(yàn)應(yīng)用到 iOS 應(yīng)用的設(shè)計(jì)上
VIPER 是我們最后一個(gè)要介紹的框架掉弛,這個(gè)框架比較有趣的是它不屬于任何一種 MV(X) 框架症见。
到目前為止,你可能覺得我們把職責(zé)劃分成三層殃饿,這個(gè)顆粒度已經(jīng)很不錯(cuò)了吧∧弊鳎現(xiàn)在 VIPER 從另一個(gè)角度對(duì)職責(zé)進(jìn)行了劃分,這次劃分了 五層乎芳。
- Interactor(交互器) - 包括數(shù)據(jù)(Entities)或者網(wǎng)絡(luò)相關(guān)的業(yè)務(wù)邏輯遵蚜。比如創(chuàng)建新的 entities 或者從服務(wù)器上獲取數(shù)據(jù)帖池;要實(shí)現(xiàn)這些功能,你可能會(huì)用到一些服務(wù)和管理(Services and Managers):這些可能會(huì)被誤以為成是外部依賴東西吭净,但是它們就是 VIPER 的 Interactor 模塊碘裕。
- Presenter(展示器) - 包括 UI(but UIKit independent)相關(guān)的業(yè)務(wù)邏輯,可以調(diào)用 Interactor 中的方法攒钳。
- Entities(實(shí)體) - 純粹的數(shù)據(jù)對(duì)象。不包括數(shù)據(jù)訪問層雷滋,因?yàn)檫@是 Interactor 的職責(zé)不撑。
- Router(路由) - 負(fù)責(zé) VIPER 模塊之間的轉(zhuǎn)場(chǎng)
實(shí)際上 VIPER 模塊可以只是一個(gè)頁面(screen),也可以是你應(yīng)用里整個(gè)的用戶使用流程(the whole user story)- 比如說「驗(yàn)證」這個(gè)功能晤斩,它可以只是一個(gè)頁面焕檬,也可以是連續(xù)相關(guān)的一組頁面。你的每個(gè)「樂高積木」想要有多大澳泵,都是你自己來決定的实愚。
如果我們把 VIPER 和 MV(X) 系列做一個(gè)對(duì)比的話,我們會(huì)發(fā)現(xiàn)它們?cè)诼氊?zé)劃分上面有下面的一些區(qū)別:
- Model(數(shù)據(jù)交互)的邏輯被轉(zhuǎn)移到了 Interactor 里面兔辅,Entities 只是一個(gè)什么都不用做的數(shù)據(jù)結(jié)構(gòu)體腊敲。
- Controller/Presenter/ViewModel 的職責(zé)里面,只有 UI 的展示功能被轉(zhuǎn)移到了 Presenter 里面维苔。Presenter 不具備直接更改數(shù)據(jù)的能力碰辅。
- VIPER 是第一個(gè)把導(dǎo)航的職責(zé)單獨(dú)劃分出來的架構(gòu)模式,負(fù)責(zé)導(dǎo)航的就是 Router 層介时。
如何正確的使用導(dǎo)航(doing routing)對(duì)于 iOS 應(yīng)用開發(fā)來說是一個(gè)挑戰(zhàn)没宾,MV(X) 系列的架構(gòu)完全就沒有意識(shí)到(所以也不用處理)這個(gè)問題。
下面的這個(gè)列子并沒有涉及到導(dǎo)航和 VIPER 模塊間的轉(zhuǎn)場(chǎng)沸柔,同樣上面 MV(X) 系列架構(gòu)里面也都沒有涉及循衰。
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: "David", lastName: "Blaine") // 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: "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 Router
let view = GreetingViewController()
let presenter = GreetingPresenter()
let interactor = GreetingInteractor()
view.eventHandler = presenter
presenter.view = view
presenter.greetingProvider = interactor
interactor.output = presenter
我們?cè)賮碓u(píng)價(jià)下它在各方面的表現(xiàn):
- 劃分 - 毫無疑問的,VIPER 在職責(zé)劃分方面是做的最好的褐澎。
- 可測(cè)性 - 理所當(dāng)然的会钝,職責(zé)劃分的越好,測(cè)試起來就越容易
- 易用 - 最后工三,你可能已經(jīng)猜到了顽素,上面兩點(diǎn)好處都是用維護(hù)性的代價(jià)換來的。一個(gè)小小的任務(wù)徒蟆,可能就需要你為各種類寫大量的接口胁出。
那么,我們到底應(yīng)該給「樂高」一個(gè)怎樣的評(píng)價(jià)呢段审?
如果你在使用 VIPER 框架的時(shí)候有一種在用樂高積木搭建帝國(guó)大廈的感覺全蝶,那么你可能 正在犯錯(cuò)誤;可能對(duì)于你負(fù)責(zé)的應(yīng)用來說,還沒有到使用 VIPER 的時(shí)候抑淫,你應(yīng)該把一些事情考慮的再簡(jiǎn)單一些绷落。總是有一些人忽視這個(gè)問題始苇,繼續(xù)扛著大炮去打小鳥砌烁。我覺得可能是因?yàn)樗麄兿嘈牛m然目前來看維護(hù)成本高的不合常理催式,但是至少在將來他們的應(yīng)用可以從 VIPER 架構(gòu)上得到回報(bào)吧函喉。如果你也跟他們的觀點(diǎn)一樣的話,那我建議你嘗試一下 Generamba - 一個(gè)可以生成 VIPER 框架的工具荣月。雖然對(duì)于我個(gè)人來講管呵,這感覺就像給大炮裝上了一個(gè)自動(dòng)瞄準(zhǔn)系統(tǒng),然后去做一件只用彈弓就能解決的事情哺窄。
結(jié)論
我們簡(jiǎn)單了解了幾種架構(gòu)模式捐下,對(duì)于那些讓你困惑的問題,我希望你已經(jīng)找到了答案萌业。但是毫無疑問坷襟,你應(yīng)該已經(jīng)意識(shí)到了,在選擇架構(gòu)模式這件問題上面生年,不存在什么 銀色子彈啤握,你需要做的就是具體情況具體分析,權(quán)衡利弊而已晶框。
因此在同一個(gè)應(yīng)用里面排抬,即便有幾種混合的架構(gòu)模式也是很正常的一件事情。比如:開始的時(shí)候授段,你用的是 MVC 架構(gòu)蹲蒲,后來你意識(shí)到有一個(gè)特殊的頁面用 MVC 做的的話維護(hù)起來會(huì)相當(dāng)?shù)穆闊贿@個(gè)時(shí)候你可以只針對(duì)這一個(gè)頁面用 MVVM 模式去開發(fā)侵贵,對(duì)于之前那些用 MVC 就能正常工作的頁面届搁,你完全沒有必要去重構(gòu)它們,因?yàn)閮煞N架構(gòu)是完全可以和睦共存的窍育。