iOS 軟件架構(gòu) - MVC, MVP, MVVM 和 VIPER 「譯」


更新:在這可以下載到NSLondon里面的Sliders代碼

在開(kāi)發(fā)過(guò)程中,是否覺(jué)得iOS的MVC軟件架構(gòu)很怪異?不知道如何切換到MVVM軟件架構(gòu)?有聽(tīng)說(shuō)過(guò)VIPER架構(gòu),但是不知道是否值得一試?

繼續(xù)往下讀,你會(huì)找到上述問(wèn)題的答案,如果找不到 —— 歡迎留言。

本文中,你將了解到關(guān)于iOS開(kāi)發(fā)中會(huì)用到的軟件架構(gòu)知識(shí)。我們會(huì)通過(guò)理論分析及小練習(xí)來(lái)評(píng)估幾個(gè)流行的iOS軟件架構(gòu)。如果希望詳細(xì)了解某些特定知識(shí)點(diǎn),可以點(diǎn)擊附帶的鏈接。

掌控軟件架構(gòu),是會(huì)讓人上癮的,因此,請(qǐng)留意:看完本文后,你可能會(huì)比看之前提出更多問(wèn)題,例如:

應(yīng)該由哪個(gè)模塊來(lái)處理網(wǎng)絡(luò)請(qǐng)求?Model 還是 Controller?

如何將 Model 的數(shù)據(jù)「?jìng)魅搿挂粋€(gè) View 的 View Model?
應(yīng)該由誰(shuí)來(lái)創(chuàng)建新的 VIPER:Router 還是 Presenter?

誰(shuí)會(huì)關(guān)心選用哪種軟件架構(gòu)?

如果不使用合適的軟件架構(gòu),那么終會(huì)有一天,你需要調(diào)試一個(gè)非常龐大,包含了眾多業(yè)務(wù)邏輯的類(lèi),那時(shí)你就會(huì)發(fā)現(xiàn)自己根本無(wú)從下手。通常來(lái)說(shuō),開(kāi)發(fā)者無(wú)法記住一個(gè)龐大的類(lèi)的所有業(yè)務(wù)邏輯,因此在分析過(guò)程中,往往會(huì)因?yàn)轭?lèi)的內(nèi)容過(guò)多而忽略掉很多重要的細(xì)節(jié)。如果你的代碼已經(jīng)遇到這樣的問(wèn)題,那么通常會(huì)是這樣:

  • 這個(gè)龐大的類(lèi)繼承了 UIViewController
  • 你的數(shù)據(jù)直接定義在 UIViewController
  • UIView中幾乎沒(méi)有處理任何業(yè)務(wù)邏輯
  • Model 是一個(gè)笨重的數(shù)據(jù)結(jié)構(gòu)
  • 單元測(cè)試代碼覆蓋不了任何業(yè)務(wù)

盡管你完全按照 Apple 的開(kāi)發(fā)指導(dǎo),并實(shí)現(xiàn)了 Apple的MVC架構(gòu),上述問(wèn)題依然會(huì)出現(xiàn)。不過(guò),問(wèn)題并非出在你的身上,而是出在蘋(píng)果的MVC架構(gòu)上,后續(xù)我們會(huì)繼續(xù)分析這個(gè)問(wèn)題。

我們先來(lái)定義什么是好的軟件架構(gòu):

  1. 軟件架構(gòu)上具有明確的分工,各個(gè)模塊的功能職責(zé)平衡分配,且明確。
  2. 良好的可測(cè)試性,通常良好的軟件架構(gòu)都具備良好的可測(cè)試性。
  3. 良好的易用性,維護(hù)成本低。

為什么需要模塊分工?

良好的模塊分工,可以大大簡(jiǎn)化我們對(duì)代碼的理解難度。雖然通過(guò)大量的開(kāi)發(fā)工作,可以訓(xùn)練我們的大腦去分析越來(lái)越復(fù)雜的邏輯,但是人總有極限,而且簡(jiǎn)單的邏輯更容易理解、不容易出錯(cuò),所以,遵循單一職責(zé)原則,將復(fù)雜的業(yè)務(wù)邏輯分解。

為什么需要良好的可測(cè)試性?

對(duì)于深知單元測(cè)試好處的開(kāi)發(fā)者來(lái)說(shuō),這并不是一個(gè)問(wèn)題。單元測(cè)試可以大大地減少程序運(yùn)行時(shí)才能發(fā)現(xiàn)的問(wèn)題,這通??梢怨?jié)省「用戶(hù)反饋」->「Bug修復(fù)」->「新版本發(fā)布」->「用戶(hù)安裝新版本」這個(gè)耗時(shí)長(zhǎng)達(dá)一周以上的過(guò)程。所以,程序的可測(cè)試性對(duì)于程序的穩(wěn)定性是異常重要的。

為什么需要良好的易用性?

毋庸置疑,最好的代碼是還沒(méi)被寫(xiě)出來(lái)的代碼。因此,越少的代碼,意味著越少的 bugs。這也意味著盡量以最少的代碼實(shí)現(xiàn)相同的功能,并非意味著這個(gè)開(kāi)發(fā)者懶惰,同時(shí),也不能不看維護(hù)成本而盲目贊同一個(gè)看似聰明的方案。

MV(X)基礎(chǔ)

現(xiàn)今,我們有幾種比較流行的軟件架構(gòu)

前三種架構(gòu)都將app分成三部分:

  • Models —— 負(fù)責(zé)數(shù)據(jù)處理及數(shù)據(jù)訪(fǎng)問(wèn),類(lèi)似 PersonPersonDataProvider 類(lèi)
  • Views —— 頁(yè)面展示層(GUI),在iOS中,所有 ui 前綴的都屬于 View
  • Controller/Presenter/ViewModel —— 在 Model 和 View之間充當(dāng)通訊媒介,通常負(fù)責(zé)兩方面,一方面當(dāng)接收到 View 的動(dòng)作通知時(shí)改變 Model,一方面當(dāng)接收到 Model 的數(shù)據(jù)變化通知時(shí)改變 View 的顯示。

將app從架構(gòu)上分成三部分有利于我們:

  • 更好地理解各部分的功能職責(zé),模塊間耦合度低
  • 更好的復(fù)用(通常 View 和 Model 可復(fù)用)
  • 更好的可測(cè)試性,可對(duì)每個(gè)部分進(jìn)行獨(dú)立測(cè)試

我們先從 MV(X) 架構(gòu)開(kāi)始分析,然后再于 VIPER 進(jìn)行對(duì)比。

MVC

如何使用

在開(kāi)始討論 Apple 版本的 MVC 架構(gòu)前,我們先看看最初的 MVC 架構(gòu)。

這個(gè)框架中,View 并非獨(dú)立,在 Model 被修改時(shí),View 只是簡(jiǎn)單地被 Controller 修改。其邏輯與網(wǎng)頁(yè)更新過(guò)程類(lèi)似:當(dāng)用戶(hù)輸入網(wǎng)址并回車(chē)后,網(wǎng)頁(yè)被重新加載,并顯示遠(yuǎn)端服務(wù)器的內(nèi)容。雖然我們可以在 iOS 中嘗試實(shí)現(xiàn)傳統(tǒng) MVC 結(jié)構(gòu)的 App,但由于此架構(gòu)有一個(gè)明顯的缺陷 —— 三個(gè)部分之間的耦合度非常高,每個(gè)部分都必須知道其他部分的具體接口與內(nèi)容。這大大降低了代碼的可重用性 —— 這不是大家希望在程序中使用的方式。因此,我們直接進(jìn)入下一環(huán)節(jié)。

傳統(tǒng)的 MVC 架構(gòu)并不適用于現(xiàn)代的 iOS 開(kāi)發(fā)。

Apple 的 MVC

預(yù)期效果

上圖中可以看出,Controller 在 View 和 Model 中充當(dāng)著「橋梁」的角色,View 和 Model 相互獨(dú)立,不需要知道任何對(duì)方的細(xì)節(jié)。雖然 Controller 的復(fù)用性很差,不過(guò)也可以接受,畢竟很多復(fù)雜的業(yè)務(wù)邏輯是不能放在 Model 里,因此也只能放到 Controller 里的。

理論上整個(gè)框架簡(jiǎn)單明了,不過(guò)你是否已經(jīng)發(fā)現(xiàn)一些端倪了?有人說(shuō),MVC 可以翻譯為 笨重的 View Controller『譯者注:原文是 Massive View Controller』。此外,view controller 的瘦身也成了iOS開(kāi)發(fā)者的一大難題。為什么在經(jīng)過(guò) Apple 改進(jìn)的 MVC 架構(gòu)中會(huì)出現(xiàn)這樣的問(wèn)題呢?

Apple 的 MVC

實(shí)際效果

在Cocoa MVC 中,由于 View 的生命周期,View 和 Controller 基本上綁定在一起,因此開(kāi)發(fā)者也只能編寫(xiě)臃腫的 View Controllers 代碼。雖然你已經(jīng)把一部分業(yè)務(wù)邏輯和數(shù)據(jù)修改操作挪到了 Model 層,但如果想對(duì) View 進(jìn)行瘦身就沒(méi)那么容易了,大部分時(shí)間 View 的職責(zé)是向 Controller 發(fā)送 action。最終,Controller 會(huì)是一個(gè)到處都是 delegate,一個(gè)臃腫的包含所有變量的 dataSouce,而且通常還需要兼顧異步網(wǎng)絡(luò)通訊的操作,還有...凡事你想到的,基本都出現(xiàn)會(huì)在 Controller 中。

下面的代碼是否似曾相似:

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

在此,本應(yīng)該屬于 View 的 cell 直接使用 Model進(jìn)行配置,換言之, MVC 架構(gòu)被打破了(MVC 中 View 與 Model 不應(yīng)該直接通訊),但在iOS開(kāi)發(fā)中,這種情況經(jīng)常出現(xiàn),而且開(kāi)發(fā)者不會(huì)覺(jué)得這樣有任何問(wèn)題。如果開(kāi)發(fā)者在開(kāi)發(fā)過(guò)程中嚴(yán)格遵循 MVC 架構(gòu),那么他們需要額外設(shè)計(jì)代碼,把 cell 配置挪到 controller 中,避免將 Model 傳遞到 View 中,這將會(huì)導(dǎo)致本來(lái)臃腫的 Controller 愈發(fā)臃腫。

Cocoa MVC 完全就是 Massive View Controller 的縮寫(xiě)。

這樣的架構(gòu)導(dǎo)致的問(wèn)題在開(kāi)發(fā)時(shí)可能不明顯,但一旦到了單元測(cè)試階段(希望你的工程有單元測(cè)試),問(wèn)題將會(huì)暴露無(wú)遺。由于你工程中的 View controller 和 View 關(guān)系緊密,設(shè)計(jì)測(cè)試用例時(shí)必須遍歷 View 顯示時(shí)的所有情況,同時(shí)需要考慮 View 的生命周期,這使得高覆蓋率的測(cè)試變得非常困難。

下面我們來(lái)看一個(gè)運(yùn)行在 playground 下的例子:『譯者注:在遵循原版的基礎(chǔ)上,譯者對(duì)代碼進(jìn)行了少許改善。運(yùn)行環(huán)境「Xcode Version 7.3 (7D175)」』

import UIKit
import XCPlayground

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(GreetingViewController.didTapButton(_:)), forControlEvents: UIControlEvents.TouchUpInside)
        
        viewLayoutInitial()
    }
    
    func didTapButton(button : UIButton!) {
        let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
        self.greetingLabel.text = greeting
    }

    func viewLayoutInitial() -> () {
        self.view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
        self.view.backgroundColor = UIColor(white: 1.0, alpha: 1.0);
        
        self.showGreetingButton.frame = CGRect(x: 10.0, y: 10.0, width: 90.0, height: 30.0)
        self.showGreetingButton.layer.cornerRadius = 6.0
        self.showGreetingButton.backgroundColor = UIColor.blueColor()
        
        self.greetingLabel.frame = CGRect(x: 10.0, y: 60.0, width: 200.0, height: 20.0)
        self.greetingLabel.textColor = UIColor.blueColor()
        self.greetingLabel.text = "Say hello to who?"
        
        self.view.addSubview(self.showGreetingButton);
        self.view.addSubview(self.greetingLabel);
    }
}

// Assembing of MVC
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
view.person = model

XCPlaygroundPage.currentPage.liveView = view.view


MVC 架構(gòu)存在于現(xiàn)在的 view controller 中

上述例子是否不大容易測(cè)試?雖然我們可以通過(guò)新建 GreetingModel 類(lèi)并將 greeting 字符串的生成代碼放到該類(lèi)中來(lái)實(shí)現(xiàn)該部分代碼的獨(dú)立測(cè)試,但如果不調(diào)用 viewDidLoad didTapButton 方法,我們很多對(duì) GreetingViewController 中 view 的顯示邏輯(雖然上述例子沒(méi)多少顯示邏輯)進(jìn)行測(cè)試。這也意味著在項(xiàng)目單元測(cè)試中,我們需要加載所有的 view,這對(duì)于單元測(cè)試來(lái)說(shuō)是很糟糕的。

實(shí)際上,在模擬器(例如 iPhone 4S)上運(yùn)行所有的 UIViews 并不能保證工程在其他設(shè)備(例如 iPad)上能正常運(yùn)行,所以我建議在 Unit Test target 配置中刪除 Host Application,直接對(duì)代碼進(jìn)行單元測(cè)試。

值得注意的是,View 和 Controller 之間的通訊基本上是不能進(jìn)行單元測(cè)試的。

綜上所述,貌似 Cocoa MVC 是一個(gè)很差的架構(gòu)。不過(guò)按照文章開(kāi)頭的論述,我們還是從三方面對(duì)其進(jìn)行分析:

  • 耦合度 —— View 和 Model 是互相獨(dú)立的,但是 View 和 View Controller 耦合度很高。
  • 可測(cè)試性 —— 只有 Model 可以脫離實(shí)際運(yùn)行環(huán)境進(jìn)行單元測(cè)試。
  • 易用性 —— 相對(duì)于其他架構(gòu),代碼量最少,而且大部分開(kāi)發(fā)者對(duì)此架構(gòu)都很熟悉,易用性良好。

如果你不想花太多時(shí)間來(lái)選擇軟件架構(gòu),并且覺(jué)得稍高的維護(hù)工作量會(huì)對(duì)你的項(xiàng)目造成很大的影響,那么,Cocoa MVC 架構(gòu)對(duì)你來(lái)說(shuō)是一個(gè)不錯(cuò)的選擇。

MVP

Cocoa MVC 希望成為的架構(gòu)

是不是看著很像 Apple 的 MVC 架構(gòu)? 但實(shí)際上此架構(gòu)的名稱(chēng)是MVP(被動(dòng)類(lèi)型 View 的變體『譯者注:原文為 Passive View variant』)。這是否意味著 Apple 的 MVC 實(shí)際上是 MVP ? 并非如此,在 Apple 的 MVC 中,View 和 Controller 是緊密耦合的,但在 MVP 中,Presenter 與 View/View Controller 完全解耦,Presenter中沒(méi)有任何與 View 布局相關(guān)的代碼,View 可以很方便地進(jìn)行移植。即便這樣,Presenter 依舊肩負(fù)著對(duì) View 的數(shù)據(jù)更新和動(dòng)作捕捉。

我要告訴你,UIViewController 實(shí)際上就是 View。

在 MVP 架構(gòu)中,繼承了 UIViewController 的子類(lèi)實(shí)際上并非 Presenter ,而是單純的 View 。這樣的分類(lèi)方式提供了極好的可測(cè)試性,與此同時(shí),由于額外實(shí)現(xiàn)設(shè)計(jì)數(shù)據(jù)和動(dòng)作之間的綁定,不可避免地會(huì)導(dǎo)致開(kāi)發(fā)量的增加。具體例子如下『譯者注:在遵循原版的基礎(chǔ)上,譯者對(duì)代碼進(jìn)行了少許改善。運(yùn)行環(huán)境「Xcode Version 7.3 (7D175)」』:

//: Playground - noun: a place where people can play

import UIKit
import XCPlayground

struct Person {     // Model
    let firstName: String
    let lastName: String
}

protocol GreetingView: class {
    func setGreeting(greeting: String)
}


protocol GreetingViewPresenter {
    init (view : GreetingViewController, person : Person)
    func showGreeting()
}

class GreetingPresenter : GreetingViewPresenter {   //Presenter
    let view : GreetingViewController
    var person : Person
    
    required init(view: GreetingViewController, 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 {  //View
    var presenter : GreetingPresenter!
    let showGreetingButton = UIButton()
    let greetingLabel = UILabel()
    
    override func viewDidLoad() {
        self.showGreetingButton.addTarget(self, action: #selector(GreetingViewController.didTapButton(_:)), forControlEvents: UIControlEvents.TouchUpInside)
        
        viewLayoutInitial()
    }
    
    func didTapButton(button : UIButton) {
        self.presenter .showGreeting()
    }
    
    func setGreeting(greeting: String) {
        self.greetingLabel.text = greeting
    }
    
    func viewLayoutInitial() {
        self.view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
        self.view.backgroundColor = UIColor(white: 1.0, alpha: 1.0);
        
        self.showGreetingButton.frame = CGRect(x: 10.0, y: 10.0, width: 90.0, height: 30.0)
        self.showGreetingButton.layer.cornerRadius = 6.0
        self.showGreetingButton.backgroundColor = UIColor.blueColor()
        
        self.greetingLabel.frame = CGRect(x: 10.0, y: 60.0, width: 200.0, height: 20.0)
        self.greetingLabel.textColor = UIColor.blueColor()
        self.greetingLabel.text = "Say hello to who?"
        
        self.view.addSubview(self.showGreetingButton);
        self.view.addSubview(self.greetingLabel);
    }
}


//Assembling of MVP
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
let presenter = GreetingPresenter(view: view, person: model)
view.presenter = presenter

XCPlaygroundPage.currentPage.liveView = view.view

關(guān)于「聚合」方式的重要說(shuō)明
由于含有三個(gè)完全獨(dú)立的模塊,MVP 是我們討論的架構(gòu)中首個(gè)暴露出模塊聚合問(wèn)題的架構(gòu)。雖然我們不希望 View 和 Model 之間有任何直接交互,但在 View 顯示時(shí)進(jìn)行模塊間的聚合顯然是不正確的,盡管我們必須在某個(gè)地方實(shí)現(xiàn)聚合。例如,我們可以創(chuàng)建一個(gè)具有完整App生命周期的「路由服務(wù)(Router service)」,專(zhuān)門(mén)負(fù)責(zé)模塊間的聚合以及 View 與 View 之間的切換?!妇酆稀箚?wèn)題會(huì)在 MVP 及接下來(lái)的架構(gòu)中一直存在并且不得不解決。

接下來(lái)我們分析一下 MVP 架構(gòu)的特點(diǎn):

  • 耦合度 —— 在此架構(gòu)中, Presenter 和 Model 模塊的職責(zé)功能最分明,同時(shí)具備一個(gè)被盡量簡(jiǎn)化的 View『譯者注:dump的意思為:盡量簡(jiǎn)化或減少?gòu)?fù)雜的內(nèi)容,另其非常容易被理解』
  • 可測(cè)試性 —— 具備非常好的可測(cè)試性,我們可以通過(guò) View 來(lái)測(cè)試大部分業(yè)務(wù)邏輯。
  • 易用性 —— 對(duì)于我們這個(gè)沒(méi)有實(shí)用性的簡(jiǎn)單例子來(lái)說(shuō),MVP 架構(gòu)的代碼量幾乎是 MVC 的兩倍,但同時(shí),MVP 在思路上更加清晰。

MVP 架構(gòu)對(duì)于 iOS 開(kāi)發(fā)來(lái)說(shuō)意味著良好的可測(cè)試性和大量的代碼

MVP

包含「綁定」和「Hooters」『譯者注:原文為「With Bindings and Hooters」,此處Hooters用詞比較隱晦,未想到合適的翻譯方式』

除了上述的 MVP 架構(gòu)外,還有一種形式的 MVP 架構(gòu) —— Supervision Controller MVP. 這個(gè) MVP 變體在 View 和 Model 間建立了直接的「綁定」關(guān)系,同時(shí),Presenter(Supervising Controller)依舊負(fù)責(zé) View 中 action 的響應(yīng)以及 View 中數(shù)據(jù)的更新。

不過(guò),此架構(gòu)的耦合度比較糟糕,View 和 Model 緊密耦合。這有點(diǎn)類(lèi)似于 Cocoa 桌面應(yīng)用開(kāi)發(fā)時(shí)遇到的狀況。

鑒于此架構(gòu)的缺陷,在此我們就不舉實(shí)際代碼例子了。

MVVM

最好的 MV(X) 架構(gòu),沒(méi)有之一

MVVM是目前來(lái)說(shuō)最新的 MV(X) 架構(gòu),希望它的出現(xiàn)能很好的解決上述架構(gòu)所面臨的問(wèn)題。

從理論上分析,Model-View-ViewModel 的架構(gòu)看起來(lái)非常完善。其中 View 和 Model 我們已經(jīng)非常熟悉了, 而 View Model 則相當(dāng)于兩者之間的中間媒介。

MVVM 與 MVP 非常類(lèi)似:

  • 在 MVVM 中,view controller 被當(dāng)作 view 來(lái)處理
  • View 和 Model 之間沒(méi)有直接的聯(lián)系

除此之外,MVVM 還使用了與 Supervising version MVP 架構(gòu)類(lèi)似的「綁定」機(jī)制;但是,這個(gè)「綁定」并非應(yīng)用于 View 和 Model 之間,而是應(yīng)用于 View 和 View Model 之間。

那么在 iOS 中,View Model 實(shí)際上是什么呢?從根本上說(shuō),View Model 是一個(gè)與 UIKit 無(wú)關(guān)的但負(fù)責(zé)控制 View 的顯示和狀態(tài)的模塊。在運(yùn)行過(guò)程中,View Model 監(jiān)聽(tīng)著 Model 的變化,并根據(jù) Model 的變化來(lái)更新自身對(duì)應(yīng)的變量,同時(shí),由于在 View 和 View Model 間設(shè)置了「綁定」,View Model 的變化也會(huì)「觸發(fā)」 View 的更新。

綁定(Bindings)

在 MVP 架構(gòu)分析的段落中,我們簡(jiǎn)短地介紹了「綁定」,在此,我們進(jìn)行更深入的討論?!附壎ā箒?lái)自于 OS X 開(kāi)發(fā),但在 iOS 中并沒(méi)有引入相關(guān)的庫(kù)。雖然在 iOS 中我們有 KVO 和 「通知(notifications)」,但就使用的便捷性來(lái)說(shuō),「綁定」還是更勝一籌。

鑒于我們不希望重復(fù)造輪子,對(duì)與「綁定」的應(yīng)用,我們有下面兩種選擇:

實(shí)際上,如果你有聽(tīng)說(shuō)過(guò) MVVM —— 你會(huì)想到 ReactiveCocoa 和 vice versa. 雖然可以通過(guò)簡(jiǎn)單的「綁定」來(lái)實(shí)現(xiàn) MVVM,但 ReactiveCocoa 能幫你更好地實(shí)現(xiàn) MVVM.

不過(guò),關(guān)于 reactive 框架,有一個(gè)殘酷的事實(shí):能力越大,責(zé)任越大『譯者注:原文為「the great power comes with the great responsibility」,估計(jì)是出自漫威「蜘蛛俠」里 Uncle Ben 說(shuō)的 「with great power comes great responsibility」』。在使用 reactive 框架時(shí),很容易把事情弄得非常復(fù)雜。換言之,一個(gè)Bug的調(diào)試可能會(huì)耗費(fèi)開(kāi)發(fā)者大量的調(diào)試時(shí)間,看看下面的棧使用情況就能猜到一二了。

殺雞焉用牛刀,對(duì)于我們簡(jiǎn)單的例子,F(xiàn)RF 和 KVO 都過(guò)于復(fù)雜,在此,我們可以直接在 ViewModel 中使用 showGreeting 函數(shù)和 greetingDidChange 回調(diào)函數(shù)來(lái)對(duì) View 進(jìn)行更新。例子如下:『譯者注:在遵循原版的基礎(chǔ)上,譯者對(duì)代碼進(jìn)行了少許改善。運(yùn)行環(huán)境「Xcode Version 7.3 (7D175)」』

import UIKit
import XCPlayground

struct Person {     // Model
    let firstName: String
    let lastName: String
}

protocol GreetingViewModelProtocol:class {
    var greeting:String? { get }
    var greetingDidChanged:((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.greetingDidChanged?(self)
        }
    }
    
    var greetingDidChanged: ((GreetingViewModelProtocol) -> ())?
    
    required init(person: Person) {
        self.person = person
        greeting = ""
    }
    
    @objc func showGreeting() {
        self.greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
    }
}

class GreetingViewController : UIViewController {
    var viewModel: GreetingViewModel! {
        didSet {
            self.viewModel.greetingDidChanged = { [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(self.viewModel.showGreeting), forControlEvents: UIControlEvents.TouchUpInside)
        
        viewLayoutInitial()
    }
    
    // layout code goes here
    func viewLayoutInitial() {
        self.view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
        self.view.backgroundColor = UIColor(white: 1.0, alpha: 1.0);
        
        self.showGreetingButton.frame = CGRect(x: 10.0, y: 10.0, width: 90.0, height: 30.0)
        self.showGreetingButton.layer.cornerRadius = 6.0
        self.showGreetingButton.backgroundColor = UIColor.blueColor()
        
        self.greetingLabel.frame = CGRect(x: 10.0, y: 60.0, width: 200.0, height: 20.0)
        self.greetingLabel.textColor = UIColor.blueColor()
        self.greetingLabel.text = "Say hello to who?"
        
        self.view.addSubview(self.showGreetingButton);
        self.view.addSubview(self.greetingLabel);
    }
}

// Assembling of MVVM
let model = Person(firstName: "David", lastName: "Blaine")
let viewModel = GreetingViewModel(person: model)
let view = GreetingViewController()
view.viewModel = viewModel

XCPlaygroundPage.currentPage.liveView = view.view

同樣的,我們按照三個(gè)標(biāo)準(zhǔn)來(lái)對(duì) MVVM 架構(gòu)進(jìn)行評(píng)判:

  • 耦合度 —— 雖然在我們的例子中并不明顯,但實(shí)際上 MVVM 中的 View 比 MVP 中的 View 具備更多的職責(zé)。在 MVVM 中 View 通過(guò)與 View Model 間的「綁定」來(lái)更新自身,而在 MVP 中,View 只是傳遞事件給 Presenter ,View 并不更新自身。
  • 可測(cè)試性 —— 鑒于View Model 對(duì) View 一無(wú)所知,我們可以很容易地對(duì) View Model 進(jìn)行單獨(dú)測(cè)試。雖然 View 同樣可以做單元測(cè)試,但需要遍歷所有頁(yè)面。
  • 易用性 —— 與 MVP 架構(gòu)相比,MVVM 具備幾乎一樣的代碼量,但在實(shí)際項(xiàng)目中,若使用 MVP 架構(gòu),你需要傳遞所有 View 上的事件到 Presenter,同時(shí)在 Presenter 中手動(dòng)更新 View ,而 MVVM 則不需要這樣的操作,所以實(shí)際使用中 MVVM 會(huì)比 MVP 輕巧很多。

MVVM 架構(gòu)非常誘人,它不僅包含了上述優(yōu)點(diǎn),同時(shí)由于「綁定」的機(jī)制,開(kāi)發(fā)者不需要為更新 View 寫(xiě)額外的代碼。除此之外,可測(cè)試性也是良好的。

VIPER

樂(lè)高建筑的理念移植到 iOS app 架構(gòu)設(shè)計(jì)中

VIPER作為我們最后一個(gè)候選架構(gòu),同時(shí)也是最有趣的架構(gòu)。

VIPER 在任務(wù)職責(zé)分層上是極好的,為了更好的進(jìn)行職責(zé)分配,VIPER 增加了 Interation 層,至此,VIPER 總共有5個(gè)分層。

  • Interactor —— 主要負(fù)責(zé)與數(shù)據(jù)或網(wǎng)絡(luò)相關(guān)的業(yè)務(wù)邏輯,例如創(chuàng)建 entity 的實(shí)例,從服務(wù)器獲取數(shù)據(jù)等。在實(shí)現(xiàn) Interactor 的過(guò)程中你可能會(huì)使用一些 Service 或 Managers ,這些并不能認(rèn)為是 VIPER 的一部分,只能說(shuō)是一些外部依賴(lài)。
  • Presenter —— 主要負(fù)責(zé) UI 相關(guān)的業(yè)務(wù)邏輯,和調(diào)用/觸發(fā) Interactor 上的接口。
  • Entities —— 純數(shù)據(jù)對(duì)象,不包含對(duì)象訪(fǎng)問(wèn)層級(jí),對(duì)象訪(fǎng)問(wèn)的邏輯歸 Interactor 管理。
  • Router —— 主要負(fù)責(zé) VIPER 各個(gè)模塊之間的數(shù)據(jù)傳遞工作。

通常來(lái)說(shuō),VIPER 可以是一個(gè)頁(yè)面,或者整個(gè) app,至于具體怎么設(shè)計(jì),完全取決于你。

如果與 MV(X) 的軟件架構(gòu)進(jìn)行對(duì)比,我們會(huì)發(fā)現(xiàn)職能分配上的一些不同:

  • Model(data interaction) 數(shù)據(jù)處理邏輯轉(zhuǎn)移到了 Interactor 模塊,Entities 成為一個(gè)純粹的數(shù)據(jù)結(jié)構(gòu)。
  • 將 Controller/Presenter/ViewModel 中只與 UI 相關(guān)的功能轉(zhuǎn)移到 Presenter 中,UI 中的數(shù)據(jù)處理依然保留在原來(lái)的模塊中。
  • VIPER 是第一個(gè)明確定義了負(fù)責(zé)頁(yè)面跳轉(zhuǎn)邏輯處理層級(jí)的架構(gòu),該層級(jí)為 Router.

在 iOS中,處理 Router 是一件非常困難的事情,但 MV(X) 架構(gòu)中不存在這個(gè)問(wèn)題

下面的例子不包含 routing 和 interaction 模塊?!鹤g者注:在遵循原版的基礎(chǔ)上,譯者對(duì)代碼進(jìn)行了少許改善。運(yùn)行環(huán)境「Xcode Version 7.3 (7D175)」』

import UIKit
import XCPlayground

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: #selector(GreetingViewController.didTapButton(_:)), forControlEvents: .TouchUpInside)
        
        self.viewLayoutInitial()
    }
    
    func didTapButton(button: UIButton) {
        self.eventHandler.didTapShowGreetingButton()
    }
    
    func setGreeting(greeting: String) {
        self.greetingLabel.text = greeting
    }
    
    // layout code goes here
    func viewLayoutInitial() {
        self.view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
        self.view.backgroundColor = UIColor(white: 1.0, alpha: 1.0);
        
        self.showGreetingButton.frame = CGRect(x: 10.0, y: 10.0, width: 90.0, height: 30.0)
        self.showGreetingButton.layer.cornerRadius = 6.0
        self.showGreetingButton.backgroundColor = UIColor.blueColor()
        
        self.greetingLabel.frame = CGRect(x: 10.0, y: 60.0, width: 200.0, height: 20.0)
        self.greetingLabel.textColor = UIColor.blueColor()
        self.greetingLabel.text = "Say hello to who?"
        
        self.view.addSubview(self.showGreetingButton);
        self.view.addSubview(self.greetingLabel);
    }
}
// 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

XCPlaygroundPage.currentPage.liveView = view.view

再次,我們通過(guò)三個(gè)維度對(duì) VIPER 架構(gòu)進(jìn)行分析:

  • 耦合度 —— 毫無(wú)疑問(wèn),VIPER 各模塊間的耦合度是最低的。
  • 可測(cè)試性 —— 同樣的,越低的耦合度,越高的可測(cè)試性。
  • 易用性 —— 盡管功能簡(jiǎn)單,開(kāi)發(fā)者還是必須額外編寫(xiě)非常多的接口類(lèi),開(kāi)發(fā)時(shí)間和維護(hù)成本很高。

什么是 LEGO ?

在使用 VIPER 時(shí),你可能會(huì)覺(jué)得自己在用 LEGO 方塊拼湊一個(gè)帝國(guó)大廈,這或許是一個(gè)「存在問(wèn)題」的信號(hào)。對(duì)于大部分開(kāi)發(fā)者來(lái)說(shuō),VIPER 顯得過(guò)于復(fù)雜以至于大家很容易就會(huì)放棄 VIPER 而尋找更簡(jiǎn)單的架構(gòu)。對(duì)于一些人來(lái)說(shuō),他們可能會(huì)繼續(xù)堅(jiān)持使用 VIPER 架構(gòu),盡管這看起來(lái)像是在用大炮打麻雀『譯者注:原文為「shooting out of cannon into sparrows」』。我覺(jué)得他們之所以愿意承受著非常高的維護(hù)代價(jià)而選擇 VIPER,應(yīng)該是他們覺(jué)得日后對(duì)他們的 app 會(huì)有很大的好處。如果你有相同的想法,不妨試試 Generamba —— 一個(gè)自動(dòng)生成 VIPER 框架的插件。對(duì)于我個(gè)人來(lái)說(shuō),這就像使用一個(gè)帶有全自動(dòng)目標(biāo)鎖定系統(tǒng)的大炮,而不是一個(gè)簡(jiǎn)易便攜的投石器『譯者注:原文為「Although for me personally it feels like using an automated targeting system for cannon instead of simply thking a sling shot.」』

結(jié)論

在分析了上述幾種常用軟件框架后,希望你可以為心中的疑問(wèn)找到答案,但毫無(wú)疑問(wèn)的說(shuō),軟件世界里沒(méi)有「尚方寶劍」『譯者注:原味為「silver bullet」,典故可參考WIKI』,選擇哪種架構(gòu),很大程度上取決于你工程的具體情況。

因此,在一個(gè) app 中使用多種軟件架構(gòu)其實(shí)是很正常的。例如你的項(xiàng)目開(kāi)始時(shí)使用的是 MVC ,后面你可能發(fā)現(xiàn)個(gè)別復(fù)雜的頁(yè)面使用 MVC 架構(gòu)實(shí)現(xiàn)時(shí)會(huì)變得難以維護(hù),此時(shí)你可能會(huì)使用 MVVM 架構(gòu)對(duì)該界面代碼進(jìn)行重構(gòu)。但并不需要修改其他使用 MVC 架構(gòu)的運(yùn)行良好的頁(yè)面代碼。

事情應(yīng)該力求簡(jiǎn)單,不過(guò)不能過(guò)于簡(jiǎn)單 —— 愛(ài)因斯坦
『譯者注:原文為「Everything Should Be Made as Simple as Possible, But Not Simpler —— Albert Einstein」』

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容