iOS 架構(gòu)模式--解密 MVC,MVP,MVVM以及VIPER架構(gòu)

本文由CocoaChina譯者lynulzy(社區(qū)ID)翻譯
作者:Bohdan Orlov
原文:iOS Architecture Patterns

為什么要關(guān)注架構(gòu)設(shè)計?

因為假如你不關(guān)心架構(gòu),那么總有一天,需要在同一個龐大的類中調(diào)試若干復(fù)雜的事情,你會發(fā)現(xiàn)在這樣的條件下,根本不可能在這個類中快速的找到以及有效的修改任何bug.當然,把這樣的一個類想象為一個整體是困難的,因此,有可能一些重要的細節(jié)總會在這個過程中會被忽略。如果現(xiàn)在的你正是處于這樣一個開發(fā)環(huán)境中,很有可能具體的情況就像下面這樣:

這個類是一個UIViewController的子類
數(shù)據(jù)直接在UIViewController中存儲
UIView類幾乎不做任何事情
Model 僅僅是一個數(shù)據(jù)結(jié)構(gòu)
單元測試覆蓋不了任何用例
以上這些情況仍舊會出現(xiàn),即使是你遵循了Apple的指導(dǎo)原則并且實現(xiàn)了其 MVC(模式,所以,大可不必驚慌。Apple所提出的 MVC 模式存在一些問題,我們之后會詳述。

在此,我們可以定義一個好的架構(gòu)應(yīng)該具備的特點:

任務(wù)均衡分攤給具有清晰角色的實體
可測試性通常都來自與上一條(對于一個合適的架構(gòu)是非常容易)
易用性和低成本維護
為什么采用分布式?

采用分布式可以在我們要弄清楚一些事情的原理時保持一個均衡的負載。如果你認為你的開發(fā)工作越多,你的大腦越能習慣復(fù)雜的思維,其實這是對的。但是,不能忽略的一個事實是,這種思維能力并不是線性增長的,而且也并不能很快的到達峰值。所以,能夠戰(zhàn)勝這種復(fù)雜性的最簡單的方法就是在遵循 單一功能原則 的前提下,將功能劃分給不同的實體。

為什么需要易測性?

其實這條要求對于哪些習慣了單元測試的人并不是一個問題,因為在添加了新的特性或者要增加一些類的復(fù)雜性之后通常會失效。這就意味著,測試可以避免開發(fā)者在運行時才發(fā)現(xiàn)問題----當應(yīng)用到達用戶的設(shè)備,每一次維護都需要浪費長達至少一周的時間才能再次分發(fā)給用戶。

為什么需要易用性?

這個問題沒有固定的答案,但值得一提的是,最好的代碼是那些從未寫過的代碼。因此,代碼寫的越少,Bug就越少。這意味著希望寫更少的代碼不應(yīng)該被單純的解釋為開發(fā)者的懶惰,而且也不應(yīng)該因為偏愛更聰明的解決方案而忽視了它的維護開銷。

MV(X)系列概要

當今我們已經(jīng)有很架構(gòu)設(shè)計模式方面的選擇:

![Uploading 1452152223816977_102409.png . . .]
MVC
MVP
MVVM
VIPER
前三種設(shè)計模式都把一個應(yīng)用中的實體分為以下三類:

Models--負責主要的數(shù)據(jù)或者操作數(shù)據(jù)的數(shù)據(jù)訪問層,可以想象 Perspn 和 PersonDataProvider 類。
Views--負責展示層(GUI),對于iOS環(huán)境可以聯(lián)想一下以 UI 開頭的所有類。
Controller/Presenter/ViewModel--負責協(xié)調(diào) Model 和 View,通常根據(jù)用戶在View上的動作在Model上作出對應(yīng)的更改,同時將更改的信息返回到View上。
將實體進行劃分給我們帶來了以下好處:

更好的理解它們之間的關(guān)系
復(fù)用(尤其是對于View和Model)
獨立的測試
讓我們開始了解MV(X)系列,之后再返回到VIPER模式。

MVC的過去

在我們探討Apple的MVC模式之前,我們來看下傳統(tǒng)的MVC模式。


傳統(tǒng)的MVC

在這里,View并沒有任何界限,僅僅是簡單的在Controller中呈現(xiàn)出Model的變化。想象一下,就像網(wǎng)頁一樣,在點擊了跳轉(zhuǎn)到某個其他頁面的連接之后就會完全的重新加載頁面。盡管在iOS平臺上實現(xiàn)這這種MVC模式是沒有任何難度的,但是它并不會為我們解決架構(gòu)問題帶來任何裨益。因為它本身也是,三個實體間相互都有通信,而且是緊密耦合的。這很顯然會大大降低了三者的復(fù)用性,而這正是我們不愿意看到的。鑒于此我們不再給出例子。

“傳統(tǒng)的MVC架構(gòu)不適用于當下的iOS開發(fā)”

蘋果推薦的MVC--愿景

Cocoa MVC

由于Controller是一個介于View 和 Model之間的協(xié)調(diào)器,所以View和Model之間沒有任何直接的聯(lián)系。Controller是一個最小可重用單元,這對我們來說是一個好消息,因為我們總要找一個地方來寫邏輯復(fù)雜度較高的代碼,而這些代碼又不適合放在Model中。

理論上來講,這種模式看起來非常直觀,但你有沒有感到哪里有一絲詭異?你甚至聽說過,有人將MVC的縮寫展開成(Massive View Controller),更有甚者,為View controller減負也成為iOS開發(fā)者面臨的一個重要話題。如果蘋果繼承并且對MVC模式有一些進展,所有這些為什么還會發(fā)生?

蘋果推薦的MVC--事實

Realistic Cocoa MVC

Cocoa的MVC模式驅(qū)使人們寫出臃腫的視圖控制器,因為它們經(jīng)常被混雜到View的生命周期中,因此很難說View和ViewController是分離的。盡管仍可以將業(yè)務(wù)邏輯和數(shù)據(jù)轉(zhuǎn)換到Model,但是大多數(shù)情況下當需要為View減負的時候我們卻無能為力了,View的最大的任務(wù)就是向Controller傳遞用戶動作事件。ViewController最終會承擔一切代理和數(shù)據(jù)源的職責,還負責一些分發(fā)和取消網(wǎng)絡(luò)請求以及一些其他的任務(wù),因此它的名字的由來...你懂的。

你可能會看見過很多次這樣的代碼:

var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell
userCell.configureWithUser(user)

這個cell,正是由View直接來調(diào)用Model,所以事實上MVC的原則已經(jīng)違背了,但是這種情況是一直發(fā)生的甚至于人們不覺得這里有哪些不對。如果嚴格遵守MVC的話,你會把對cell的設(shè)置放在 Controller 中,不向View傳遞一個Model對象,這樣就會大大增加Controller的體積。

“Cocoa 的MVC被寫成Massive View Controller 是不無道理的。”

直到進行單元測試的時候才會發(fā)現(xiàn)問題越來越明顯。因為你的ViewController和View是緊密耦合的,對它們進行測試就顯得很艱難--你得有足夠的創(chuàng)造性來模擬View和它們的生命周期,在以這樣的方式來寫View Controller的同時,業(yè)務(wù)邏輯的代碼也逐漸被分散到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: "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可以在一個正在顯示的ViewController中實現(xiàn)”

這段代碼看起來可測試性并不強,我們可以把和greeting相關(guān)的都放到GreetingModel中然后分開測試,但是這樣我們就無法通過直接調(diào)用在GreetingViewController中的UIView的方法(viewDidLoad和didTapButton方法)來測試頁面的展示邏輯了,因為一旦調(diào)用則會使整個頁面都變化,這對單元測試來講并不是什么好消息。

事實上,在單獨一個模擬器中(比如iPhone 4S)加載并測試UIView并不能保證在其他設(shè)備中也能正常工作,因此我建議在單元測試的Target的設(shè)置下移除"Host Application"項,并且不要在模擬器中測試你的應(yīng)用。

“View和Controller的接口并不適合單元測試。”

以上所述,似乎Cocoa MVC 看起來是一個相當差的架構(gòu)方案。我們來重新評估一下文章開頭我們提出的MVC一系列的特征:

任務(wù)均攤--View和Model確實是分開的,但是View和Controller卻是緊密耦合的
可測試性--由于糟糕的分散性,只能對Model進行測試
易用性--與其他幾種模式相比最小的代碼量。熟悉的人很多,因而即使對于經(jīng)驗不那么豐富的開發(fā)者來講維護起來也較為容易。
如果你不想在架構(gòu)選擇上投入更多精力,那么Cocoa MVC無疑是最好的方案,而且你會發(fā)現(xiàn)一些其他維護成本較高的模式對于你所開發(fā)的小的應(yīng)用是一個致命的打擊。

“就開發(fā)速度而言,Cocoa MVC是最好的架構(gòu)選擇方案。”

MVP 實現(xiàn)了Cocoa的MVC的愿景

Passive View variant of MVP

這看起來不正是蘋果所提出的MVC方案嗎?確實是的,這種模式的名字叫做MVC,但是,這就是說蘋果的MVC實際上就是MVP了?不,并不是這樣的。如果你仔細回憶一下,View是和Controller緊密耦合的,但是MVP的協(xié)調(diào)器Presenter并沒有對ViewController的生命周期做任何改變,因此View可以很容易的被模擬出來。在Presenter中根本沒有和布局有關(guān)的代碼,但是它卻負責更新View的數(shù)據(jù)和狀態(tài)。

但是,“假如告訴你UIViewController就是View呢?”

就MVP而言,UIViewController的子類實際上就是Views并不是Presenters。這點區(qū)別使得這種模式的可測試性得到了極大的提高,付出的代價是開發(fā)速度的一些降低,因為必須要做一些手動的數(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是第一個如何協(xié)調(diào)整合三個實際上分離的層次的架構(gòu)模式,既然我們不希望View涉及到Model,那么在顯示的View Controller(其實就是View)中處理這種協(xié)調(diào)的邏輯就是不正確的,因此我們需要在其他地方來做這些事情。例如,我們可以做基于整個App范圍內(nèi)的路由服務(wù),由它來負責執(zhí)行協(xié)調(diào)任務(wù),以及View到View的展示。這個出現(xiàn)并且必須處理的問題不僅僅是在MVP模式中,同時也存在于以下集中方案中。

我們來看下MVP模式下的三個特性的分析:

任務(wù)均攤--我們將最主要的任務(wù)劃分到Presenter和Model,而View的功能較少(雖然上述例子中Model的任務(wù)也并不多)。
可測試性--非常好,由于一個功能簡單的View層,所以測試大多數(shù)業(yè)務(wù)邏輯也變得簡單
易用性--在我們上邊不切實際的簡單的例子中,代碼量是MVC模式的2倍,但同時MVP的概念卻非常清晰
“iOS 中的MVP意味著可測試性強、代碼量大。”

MVP--綁定和信號

還有一些其他形態(tài)的MVP--監(jiān)控控制器的MVP。

這個變體包含了View和Model之間的直接綁定,但是Presenter仍然來管理來自View的動作事件,同時也能勝任對View的更新。


Supervising Presenter variant of the MVP

但是我們之前就了解到,模糊的職責劃分是非常糟糕的,更何況將View和Model緊密的聯(lián)系起來。這和Cocoa的桌面開發(fā)的原理有些相似。

和傳統(tǒng)的MVC一樣,寫這樣的例子沒有什么價值,故不再給出。

MVVM--最新且是最偉大的MV(X)系列的一員

MVVM架構(gòu)是MV(X)系列最新的一員,因此讓我們希望它已經(jīng)考慮到MV(X)系列中之前已經(jīng)出現(xiàn)的問題。

從理論層面來講MVVM看起來不錯,我們已經(jīng)非常熟悉View和Model,以及Meditor,在MVVM中它是View Model。

MVVM

它和MVP模式看起來非常像:

MVVM將ViewController視作View
在View和Model之間沒有緊密的聯(lián)系
此外,它還有像監(jiān)管版本的MVP那樣的綁定功能,但這個綁定不是在View和Model之間而是在View和ViewModel之間。

那么問題來了,在iOS中ViewModel實際上代表什么?它基本上就是UIKit下的每個控件以及控件的狀態(tài)。ViewModel調(diào)用會改變Model同時會將Model的改變更新到自身并且因為我們綁定了View和ViewModel,第一步就是相應(yīng)的更新狀態(tài)。

綁定

我在MVP部分已經(jīng)提到這點了,但是該部分我們?nèi)詴^續(xù)討論。

如果我們自己不想自己實現(xiàn),那么我們有兩種選擇:

基于KVO的綁定庫如 RZDataBinding 和 SwiftBond
完全的函數(shù)響應(yīng)式編程,比如像ReactiveCocoa、RxSwift或者 PromiseKit
事實上,尤其是最近,你聽到MVVM就會想到ReactiveCoca,反之亦然。盡管通過簡單的綁定來使用MVVM是可實現(xiàn)的,但是ReactiveCocoa卻能更好的發(fā)揮MVVM的特點。

但是關(guān)于這個框架有一個不得不說的事實:強大的能力來自于巨大的責任。當你開始使用Reactive的時候有很大的可能就會把事情搞砸。換句話來說就是,如果發(fā)現(xiàn)了一些錯誤,調(diào)試出這個bug可能會花費大量的時間,看下函數(shù)調(diào)用棧:

Reactive Debugging

在我們簡單的例子中,F(xiàn)RF框架和KVO被過渡禁用,取而代之地我們直接去調(diào)用showGreeting方法更新ViewModel,以及通過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: "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

讓我們再來看看關(guān)于三個特性的評估:

任務(wù)均攤 -- 在例子中并不是很清晰,但是事實上,MVVM的View要比MVP中的View承擔的責任多。因為前者通過ViewModel的設(shè)置綁定來更新狀態(tài),而后者只監(jiān)聽Presenter的事件但并不會對自己有什么更新。
可測試性 -- ViewModel不知道關(guān)于View的任何事情,這允許我們可以輕易的測試ViewModel。同時View也可以被測試,但是由于屬于UIKit的范疇,對他們的測試通常會被忽略。
易用性 -- 在我們例子中的代碼量和MVP的差不多,但是在實際開發(fā)中,我們必須把View中的事件指向Presenter并且手動的來更新View,如果使用綁定的話,MVVM代碼量將會小的多。
“MVVM很誘人,因為它集合了上述方法的優(yōu)點,并且由于在View層的綁定,它并不需要其他附加的代碼來更新View,盡管這樣,可測試性依然很強。”

VIPER--把LEGO建筑經(jīng)驗遷移到iOS app的設(shè)計

VIPER是我們最后要介紹的,由于不是來自于MV(X)系列,它具備一定的趣味性。

迄今為止,劃分責任的粒度是很好的選擇。VIPER在責任劃分層面進行了迭代,VIPER分為五個層次:

VIPER

交互器 -- 包括關(guān)于數(shù)據(jù)和網(wǎng)絡(luò)請求的業(yè)務(wù)邏輯,例如創(chuàng)建一個實體(數(shù)據(jù)),或者從服務(wù)器中獲取一些數(shù)據(jù)。為了實現(xiàn)這些功能,需要使用服務(wù)、管理器,但是他們并不被認為是VIPER架構(gòu)內(nèi)的模塊,而是外部依賴。
展示器 -- 包含UI層面的業(yè)務(wù)邏輯以及在交互器層面的方法調(diào)用。
實體 -- 普通的數(shù)據(jù)對象,不屬于數(shù)據(jù)訪問層次,因為數(shù)據(jù)訪問屬于交互器的職責。
路由器 -- 用來連接VIPER的各個模塊。
基本上,VIPER模塊可以是一個屏幕或者用戶使用應(yīng)用的整個過程--想想認證過程,可以由一屏完成或者需要幾步才能完成,你的模塊期望是多大的,這取決于你。

當我們把VIPER和MV(X)系列作比較時,我們會在任務(wù)均攤性方面發(fā)現(xiàn)一些不同:

Model 邏輯通過把實體作為最小的數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換到交互器中。
Controller/Presenter/ViewModel的UI展示方面的職責移到了Presenter中,但是并沒有數(shù)據(jù)轉(zhuǎn)換相關(guān)的操作。
VIPER是第一個通過路由器實現(xiàn)明確的地址導(dǎo)航模式。
“找到一個適合的方法來實現(xiàn)路由對于iOS應(yīng)用是一個挑戰(zhàn),MV(X)系列避開了這個問題。”

例子中并不包含路由和模塊之間的交互,所以和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

讓我們再來評估一下特性:

任務(wù)均攤 -- 毫無疑問,VIPER是任務(wù)劃分中的佼佼者。
可測試性 -- 不出意外地,更好的分布性就有更好的可測試性。
易用性 -- 最后你可能已經(jīng)猜到了維護成本方面的問題。你必須為很小功能的類寫出大量的接口。
什么是LEGO

當使用VIPER時,你的感覺就像是用樂高積木來搭建一個城堡,這也是一個表明當前存在一些問題的信號。可能現(xiàn)在就應(yīng)用VIPER架構(gòu)還為時過早,考慮一些更為簡單的模式可能會更好。一些人會忽略這些問題,大材小用。假定他們篤信VIPER架構(gòu)會在未來給他們的應(yīng)用帶來一些好處,雖然現(xiàn)在維護起來確實是有些不合理。如果你也持這樣的觀點,我為你推薦 Generamba 這個用來搭建VIPER架構(gòu)的工具。雖然我個人感覺,使用起來就像加農(nóng)炮的自動瞄準系統(tǒng),而不是簡單的像投石器那樣的簡單的拋擲。

總結(jié)

我們了解了集中架構(gòu)模式,希望你已經(jīng)找到了到底是什么在困擾你。毫無疑問通過閱讀本篇文章,你已經(jīng)了解到其實并沒有完全的銀彈。所以選擇架構(gòu)是一個根據(jù)實際情況具體分析利弊的過程。

因此,在同一個應(yīng)用中包含著多種架構(gòu)。比如,你開始的時候使用MVC,然后突然意識到一個頁面在MVC模式下的變得越來越難以維護,然后就切換到MVVM架構(gòu),但是僅僅針對這一個頁面。并沒有必要對哪些MVC模式下運轉(zhuǎn)良好的頁面進行重構(gòu),因為二者是可以并存的。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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