iOS架構設計:揭祕MVC, MVP, MVVM以及VIPER

不要錯過最新的iOS開發技能樹 —— github地址ios

更新:在這裏能夠看到幻燈片 在iOS中使用MVC時感受怪怪的?對切換到MVVM有疑慮?據說過VIPER,但不知道是否值得?git

往下看,你將會找到這些問題的答案,若是還有疑問,請在評論區留言。github

你將瞭解到在iOS環境下如何進行系統架構設計。咱們將簡單回顧一些流行的框架,並經過實踐一些小例子來比較它們的理論。若是須要更多詳細信息,請參考文章中出現的連接。編程

掌握設計模式可能會讓人上癮,因此要當心:你可能在閱讀這篇文章以前已經問過本身一些問題,好比說: 誰應該擁有聯網請求:Model仍是Controller? 如何將Model傳遞到新View的View Model中? 誰建立了一個新的VIPER模塊:Router仍是Presenter?設計模式

爲何要糾結選擇什麼架構呢?

假若有一天,你在調試一個實現了幾十種功能的龐大的類時,你會發現本身很難找到並修復你的類中的任何錯誤。而且,很難把這個類做爲一個總體來考慮,所以,你總會忽略一些重要的細節。若是你的應用程序中已經出現了這種狀況,那麼頗有可能:bash

  • 這類是UIViewController類。
  • UIViewController直接存儲和處理你的數據
  • 你的UIView中幾乎沒有作任何事情
  • Model僅僅是一個數據結構
  • 單元測試覆蓋不了任何內容

即便你遵循了蘋果的指導方針並實現了蘋果的MVC模式,這種狀況仍是會發生的,因此不要難過。蘋果的MVC有點問題,這個咱們稍後再談。服務器

讓咱們定義一個優秀系統結構的特徵: 1.角色間職責的清晰分配(分佈式)。 2.可測試性一般來自第一個特性(沒必要擔憂:使用適當的系統結構是很容易的)。 3.使用方便,維護成本低。網絡

爲何要採用分佈式

當咱們想弄清楚某些事情是如何運做時,採用分佈式能讓咱們的大腦思路清晰。若是你認爲你開發越多,你的大腦就越能理解複雜性,那麼你是對的。但這種能力不是線性的,很快就會達到上限。所以,克服複雜性的最簡單方法是按照單一職責原則在多個實體之間劃分職責。數據結構

爲何要可測試

對於那些已經習慣了單元測試的人來講,這一般不是問題,由於在添加了新的特性或者要增長一些類的複雜性以後一般會失敗。這意味着測試可以下降應用程序在用戶的設備上發生問題的機率,那時修復也許須要一個星期(審覈)才能到達用戶。架構

爲何要易用性

這並不須要回答,但值得一提的是,最好的代碼是從未編寫過的代碼。所以,你擁有的代碼越少,你擁有的bug就越少。這意味着編寫更少代碼的願望決不能僅僅由開發人員的懶惰來解釋,你不該該偏心看起來更聰明的解決方案而忽視它的維護成本。

MV(X) 簡介

如今咱們在架構設計模式上有不少選擇:

他們中的三個假設將應用程序的實體分紅3類:

  • Models — 負責保存數據或數據訪問層,操縱數據,例如「人」或「提供數據的人」。
  • Views  —  負責表示層(GUI),iOS環境下一般以「UI」前綴。
  • Controller/Presenter/ViewModel  —  Model和View之間的中介,通常負責在用戶操做View時更新Model,以及當Model變化時更新View。

這種劃分能讓咱們:

  • 更好地理解它們(如咱們所知)
  • 重用它們(尤爲是View和Model)
  • 獨立地進行測試(單元測試)

讓咱們從MV(X)開始,稍後在回到VIPER:

MVC

曾經

在討論蘋果對MVC的見解以前,讓咱們先看看傳統的MVC。

傳統的MVC

在上圖的狀況下,View是無狀態的。一旦Model被改變,Controller就會簡單地渲染它。例如:網頁徹底加載後,一旦你按下連接,就導航到其餘地方。
雖然在iOS應用用傳統的MVC架構也能夠實現,但這並無多大意義,因爲架構問題 ——三個實體是緊耦合的,每一個實體和其餘兩個通訊。這大大下降了可重用性——這可不是你但願在你的應用程序看到的。出於這個緣由,咱們甚至不想編寫規範的MVC示例。

傳統的MVC彷佛不適用於現代IOS開發。

蘋果的MVC

願景:

Cocoa MVC

Controller是View和Model之間的中介,這樣他們就解耦了。最小的可重用單元是Controller,這對咱們來講是個好消息,由於咱們必須有一個來放那些不適合放入Model的複雜業務邏輯的地方。
從理論上講,它看起來很簡單,但你以爲有些地方不對,對吧?你甚至聽到有人說MVC全稱應該改成Massive View Controller(大量的視圖控制器)。此外,爲View controller減負也成爲iOS開發者面臨的一個重要話題。
若是蘋果只接受傳統的MVC並改進了它,爲何會出現這種狀況呢?

實際狀況:

事實上的Cocoa MVC

Cocoa MVC鼓勵人們編寫大規模的視圖控制器,並且因爲它們涉及View的生命週期,因此很難說它們(View和Controller)是分離的。
雖然你仍有能力將一些業務邏輯和數據轉換成Model,但你沒辦法將View從Controller中分離。在大多數時候全部View的責任是把事件傳遞給Controller。
ViewController最終演變成一個其餘人的delegate和data source,一般負責分派和取消網絡請求…你明白的。
你見過多少這樣的代碼?:

var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell
userCell.configureWithUser(user)
複製代碼

Cell(一個View)跟一個Model直接綁定了!因此MVC準則被違反了,可是這種狀況老是發生,一般人們不會以爲它是錯誤的。若是你嚴格遵循MVC,那麼你應該從Controller配置cell,而不是將Model傳遞到cell中,這將增大Controller。

Cocoa MVC 的全稱應該是 Massive View Controller.

在單元測試以前,這個問題可能並不明顯(但願在你的項目中是這樣)。
因爲視圖控制器與視圖緊密耦合,所以很難測試——由於在編寫視圖控制器的代碼時,你必須模擬View的生命週期,從而使你的業務邏輯儘量地與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
        
    }
    // 這裏寫佈局代碼
}
// 組裝MVC
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
view.person = model;
複製代碼

MVC在可見的ViewController中進行組裝

這彷佛不太容易測試,對嗎?
咱們能夠將greeting移動到新的GreetingModel類中並分別進行測試,但咱們不能在不調用GreetingViewController的有關方法(viewDidLoad, didTapButton,這將會加載全部的View) 的狀況下測試UIView中的顯示邏輯(雖然在上面的例子中沒有太多這樣的邏輯)。這不利於單元測試。
事實上,在一個模擬器(如iPhone 4S)中測試UIViews並不能保證它會在其餘設備良好的工做(例如iPad),因此我建議從你的單元測試Target中刪除「Host Application」選項,而後脫離應用程序運行你的測試。

View和Controller之間的交互在單元測試中是不可測試的。

如此看來,Cocoa MVC 模式 彷佛是一個很糟糕的選擇。可是讓咱們根據文章開頭定義的特性來評估它:

  • 職責拆分 — View和Model實現了分離,可是View與Controller還是緊耦合。
  • 可測性 — 因爲模式的緣由,你只能測試你的Model。
  • 易用性 — 相比於其餘模式代碼量最少。此外,每一個人都熟悉它,即便經驗不太豐富的開發人員也可以維護它。

若是你不肯意在項目的架構上投入太多的時間,那麼Cocoa MVC 就是你應該選擇的模式。並且你會發現用其餘維護成本較高的模式開發小的應用是一個致命的錯誤。

Cocoa MVC是開發速度最快的架構模式。

MVP

MVP 實現了Cocoa的MVC的願景

Passive View 變體 — MVP

這看起來不正是蘋果的MVC嗎?是的,它的名字是MVP(Passive View variant,被動視圖變體)。等等...這是否是意味着蘋果的MVC其實是MVP?不,不是這樣。若是你仔細回憶一下,View是和Controller緊密耦合的,可是MVP的中介Presenter並無對ViewController的生命週期作任何改變,所以View能夠很容易的被模擬出來。在Presenter中根本沒有和佈局有關的代碼,可是它卻負責更新View的數據和狀態。

假如告訴你,UIViewController就是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: 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
    }
    
    // 佈局代碼
}
// 裝配 MVP
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
let presenter = GreetingPresenter(view: view, person: model)
view.presenter = presenter
複製代碼

裝配問題的重要說明

MVP是第一個揭示裝配問題的模式,由於它有三個獨立的層。既然咱們不但願View和Model耦合,那麼在顯示的View Controller(其實就是View)中處理這種協調的邏輯就是不正確的,所以咱們須要在其餘地方來作這些事情。例如,咱們能夠作基於整個App範圍內的路由服務,由它來負責執行協調任務,以及View到View的展現。這不只僅是在MVP模式中必須處理的問題,同時也存在於如下集中方案中。

讓咱們看看MVP的特色:

  • 職責拆分 — 咱們將最主要的任務劃分到Presenter和Model,而View的功能較少(雖然上述例子中Model的任務也並很少)。
  • 可測性 — 很是好,基於一個功能簡單的View層,能夠測試大多數業務邏輯
  • 易用性 — 在咱們上邊不切實際的簡單的例子中,代碼量是MVC模式的2倍,但同時MVP的概念卻很是清晰。

iOS 中的MVP意味着可測試性強、代碼量大。

MVP

關於Bindings和Hooters

還有一些其餘形態的MVP —— Supervising Controller MVP(監聽Controller的MVP)。這個變體的變化包括View和Model之間的直接綁定,可是Presenter(Supervising Controller)仍然來管理來自View的動做事件,同時也能勝任對View的更新。

監聽Controller的MVP

可是咱們以前就瞭解到,模糊的職責劃分是很是糟糕的,更況且將View和Model緊密的聯繫起來。這和Cocoa的桌面開發的原理有些類似。

和傳統的MVC同樣,寫這樣的例子沒有什麼價值,故再也不給出。

MVVM

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

MVVM架構是MV(X)系列最新的成員,咱們但願它已經考慮到MV(X)系列中以前已經出現的問題。
從理論層面來說Model-View-ViewModel看起來不錯,咱們已經很是熟悉View和Model,以及Meditor(中介),在這裏它叫作View Model。

MVVM

它和MVP模式看起來很像:

  • MVVM也將ViewController視做View
  • 在View和Model之間沒有耦合

此外,它還有像Supervising版本的MVP那樣的綁定功能,但這個綁定不是在View和Model之間而是在View和ViewModel之間。

那麼在iOS中ViewModel到底表明了什麼?它基本上就是UIKit下的獨立控件以及控件的狀態。ViewModel調用會改變Model同時會將Model的改變動新到自身而且由於咱們綁定了View和ViewModel,第一步就是相應的更新狀態。

綁定

我在MVP部分已經提到這點了,可是在這裏咱們來繼續討論。
綁定是從OS X開發中衍生出來的,可是咱們沒有在iOS開發中使用它們。固然咱們有KVO通知,但它們沒有綁定方便。
若是咱們本身不想本身實現,那麼咱們有兩種選擇:

事實上,尤爲是最近,你聽到MVVM就會想到ReactiveCoca,反之亦然。儘管經過簡單的綁定來使用MVVM是可實現的,可是ReactiveCocoa(或其變體)卻能更好的發揮MVVM的特色。

函數響應式框架有一個殘酷的事實:強大的能力來自於巨大的責任。當你開始使用Reactive的時候有很大的可能就會把事情搞砸。換句話來講就是,若是發現了一些錯誤,調試出這個bug可能會花費大量的時間,看下函數調用棧:

Reactive Debugging
在咱們簡單的例子中,FRF框架和KVO被禁用,取而代之地咱們直接去調用showGreeting方法更新ViewModel,以及經過greetingDidChange 回調函數使用屬性。

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
}
// 裝配 MVVM
let model = Person(firstName: "David", lastName: "Blaine")
let viewModel = GreetingViewModel(person: model)
let view = GreetingViewController()
view.viewModel = viewModel
複製代碼

讓咱們再來看看關於三個特性的評估:

  • 職責拆分 — 在例子中並非很清晰,可是事實上,MVVM的View要比MVP中的View承擔的責任多。由於前者經過ViewModel的設置綁定來更新狀態,然後者只監聽Presenter的事件但並不會對本身有什麼更新。
  • 可測性 — ViewModel不知道關於View的任何事情,這容許咱們能夠輕易的測試ViewModel。同時View也能夠被測試,可是因爲屬於UIKit的範疇,對他們的測試一般會被忽略。
  • 易用性 — 在咱們例子中的代碼量和MVP的差很少,可是在實際開發中,咱們必須把View中的事件指向Presenter而且手動的來更新View,若是使用綁定的話,MVVM代碼量將會小的多。

MVVM是很是有吸引力的,由於它集合了上述方法的優勢,而且因爲在View層的綁定,它並不須要其餘附加的代碼來更新View,儘管這樣,可測試性依然很強。

VIPER

把LEGO架構經驗遷移到iOS app的設計

VIPER是咱們最後要介紹的,因爲不是來自於MV(X)系列,它具有必定的趣味性。

到目前爲止,你必須贊成劃分責任的粒度是很好的選擇。VIPER在責任劃分層面進行了迭代,VIPER分爲五個層次:

VIPER

  • 交互器(Interactor) — 包括關於數據和網絡請求的業務邏輯,例如建立一個實體(Entities),或者從服務器中獲取一些數據。爲了實現這些功能,須要使用服務、管理器,可是他們並不被認爲是VIPER架構內的模塊,而是外部依賴。
  • 展現器(Presenter) — 包含UI層面(但UIKit獨立)的業務邏輯以及在交互器(Interactor)層面的方法調用。
  • 實體(Entities) — 普通的數據對象,不屬於數據訪問層,由於數據訪問屬於交互器(Interactor)的職責。
  • 路由器(Router) — 用來鏈接VIPER的各個模塊。

基本上,VIPER的模塊能夠是一個屏幕或者用戶使用應用的整個過程 —— 例如認證過程,能夠由一屏完成或者須要幾步才能完成。你想讓模塊多大,這取決於你。

當咱們把VIPER和MV(X)系列做比較時,咱們會在職責劃分方面發現一些不一樣:

  • Model(數據交互)邏輯以實體(Entities)爲單位拆分到交互器(Interactor)中。
  • Controller/Presenter/ViewModel 的UI展現方面的職責移到了Presenter中,可是並無數據轉換相關的操做。
  • VIPER 是第一個經過路由器(Router)實現明確的地址導航的模式。

找到一個適合的方法來實現路由對於iOS應用是一個挑戰,MV(X)系列並未涉及這一問題。

例子中並不包含路由和模塊之間的交互,因此和MV(X)系列部分架構同樣再也不給出例子。

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
    }
    
    // 佈局代碼
}
// 裝配 VIPER 模塊(不包含路由)
let view = GreetingViewController()
let presenter = GreetingPresenter()
let interactor = GreetingInteractor()
view.eventHandler = presenter
presenter.view = view
presenter.greetingProvider = interactor
interactor.output = presenter
複製代碼

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

  • 職責拆分 — 毫無疑問,VIPER是任務劃分中的佼佼者。
  • 可測性 — 不出意外地,更好的分佈性就有更好的可測試性。
  • 易用性 — 最後你可能已經猜到了維護成本方面的問題。你必須爲很小功能的類寫出大量的接口。

什麼是LEGO

當使用VIPER時,你可能想像用樂高積木來搭建一個城堡,這個想法可能存在一些問題。
也許,如今就應用VIPER架構還爲時過早,考慮一些更爲簡單的模式反而會更好。一些人會忽略這些問題,大材小用。假定他們篤信VIPER架構會在將來給他們的應用帶來一些好處,雖然如今維護起來確實是有些費勁。若是你也持這樣的觀點,我爲你推薦 Generamba 這個用來搭建VIPER架構的工具。雖然我我的感受這是在用高射炮打蚊子。

總結

咱們研究了幾種架構模式,但願你能找到一些困擾你的問題的答案。但毫無疑問經過閱讀這篇文章你應該已經認識到了沒有絕對的解決方案。因此架構模式的選擇須要根據實際狀況進行利弊分析。
所以,在同一應用程序中混合架構是很天然的。例如:你開始的時候使用MVC,而後忽然意識到一個頁面在MVC模式下的變得愈來愈難以維護,而後就切換到MVVM架構,可是僅僅針對這一個頁面。並無必要對哪些MVC模式下運轉良好的頁面進行重構,由於兩者是能夠並存的。

讓一切儘量簡單,但不是愚蠢。  ——  阿爾伯特·愛因斯坦

譯自:iOS Architecture Patterns

相關文章
相關標籤/搜索