iOS 架構(gòu)模式 - 簡(jiǎn)述 MVC, MVP, MVVM 和 VIPER (譯)

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)該具有的特征:

  1. 用嚴(yán)格定義的角色,平衡的將職責(zé) 劃分 給不同的實(shí)體够话。
  2. 可測(cè)性 通常取決于上面說的第一點(diǎn)(不用太擔(dān)心,如果架構(gòu)何時(shí)的話光绕,做到這點(diǎn)并不難)女嘲。
  3. 易用 并且維護(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)是完全可以和睦共存的窍育。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末卡睦,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子漱抓,更是在濱河造成了極大的恐慌表锻,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,651評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乞娄,死亡現(xiàn)場(chǎng)離奇詭異瞬逊,居然都是意外死亡显歧,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門确镊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來士骤,“玉大人,你說我怎么就攤上這事蕾域】郊。” “怎么了?”我有些...
    開封第一講書人閱讀 162,931評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵旨巷,是天一觀的道長(zhǎng)巨缘。 經(jīng)常有香客問我,道長(zhǎng)契沫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,218評(píng)論 1 292
  • 正文 為了忘掉前任昔汉,我火速辦了婚禮懈万,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘靶病。我一直安慰自己会通,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評(píng)論 6 388
  • 文/花漫 我一把揭開白布娄周。 她就那樣靜靜地躺著涕侈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪煤辨。 梳的紋絲不亂的頭發(fā)上裳涛,一...
    開封第一講書人閱讀 51,198評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音众辨,去河邊找鬼端三。 笑死,一個(gè)胖子當(dāng)著我的面吹牛鹃彻,可吹牛的內(nèi)容都是我干的郊闯。 我是一名探鬼主播,決...
    沈念sama閱讀 40,084評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蛛株,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼团赁!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起谨履,我...
    開封第一講書人閱讀 38,926評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤欢摄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后笋粟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體剧浸,經(jīng)...
    沈念sama閱讀 45,341評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡锹引,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了唆香。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嫌变。...
    茶點(diǎn)故事閱讀 39,731評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖躬它,靈堂內(nèi)的尸體忽然破棺而出腾啥,到底是詐尸還是另有隱情,我是刑警寧澤冯吓,帶...
    沈念sama閱讀 35,430評(píng)論 5 343
  • 正文 年R本政府宣布倘待,位于F島的核電站,受9級(jí)特大地震影響组贺,放射性物質(zhì)發(fā)生泄漏凸舵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評(píng)論 3 326
  • 文/蒙蒙 一失尖、第九天 我趴在偏房一處隱蔽的房頂上張望啊奄。 院中可真熱鬧,春花似錦掀潮、人聲如沸菇夸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽庄新。三九已至,卻和暖如春薯鼠,著一層夾襖步出監(jiān)牢的瞬間择诈,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工出皇, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留吭从,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,743評(píng)論 2 368
  • 正文 我出身青樓恶迈,卻偏偏與公主長(zhǎng)得像涩金,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子暇仲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容