[翻譯] iOS架構模式 — 揭開MVC, MVP, MVVM, VIPER的神秘面紗

這篇瀏覽量突然就增多了嚇一跳( ⊙ o ⊙ ) 想在前面寫一句,我還是小弱,可能難免有翻譯出問題的地方,有疑問的話請翻閱一下原稿,錯誤不足之處敬請指正,謝謝謝謝!


原作者Bohdan Orlov,原稿在這里:https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52#.62qdo7sak
在微博上看到很多人轉發推薦這篇文章,我也一直想研究一下架構模式,就去看了,順帶翻譯了。翻譯到一半發現好長啊...不過還是堅持一口氣寫完了哈哈哈哈,開心O(∩_∩)O!


在iOS中使用MVC感到別扭嗎?猶豫要換成MVVM嗎?聽說過VIPER,但是不知道它究竟有什么好處嗎?
繼續讀下去,你就可以知道這些問題的答案,如果沒有,可以在評論區隨意抱怨喔。
接下來你將了解到iOS環境中架構模式的基本知識。我們會簡要介紹一些常用的模式,并且用小例子來說明實際的用法。如果你需要知道更多的細節,可以點擊相應的鏈接查看。
學習設計模式可是會上癮的喔。看完這篇文章以后你可能會有比看之前更多的問題,比如:

  • 誰應該來擁有一個網絡請求呢?模型還是控制器?
  • 我應該怎樣把一個模型傳入一個新視圖的視圖模型里呢?
  • 誰來新建一個VIPER模塊,路由器還是表達者?

<br />

為什么要關心選擇怎樣的架構呢?

因為如果你不關心,有一天當你面對浩如煙海的類和bug的時候,你會束手無策冗不見治。你不可能記住你寫的每個類,很容易忘記一些細節。也許你已經處于這種狀況下了,比如說你很可能:

  • 寫了一個UIViewController的子類
  • 你把數據直接存在UIViewController里了
  • 你的UIView基本沒做任何事
  • 你的模型是一個死板的數據結構
  • 你的單元測試什么都沒干

這些都是有可能發生的,就算你遵守了蘋果的MVC模式指導,仍然可能會有這些情況,很正常,不要太難過喔。這是蘋果MVC自己存在的問題,我們一會兒會說到。
首先來看一個優秀的架構應該是怎樣的:

  1. 每個實體都有嚴格的角色確定,他們的任務分配是很明確而均勻的。
  2. 有了第一條,可測性一般也就有啦。
  3. 易于使用,易于維護。
為什么要分配?

分配使我們的大腦在想事情的時候任務量比較均衡。如果你覺得腦子越用越聰明,越能應對復雜的事物,你是對的。但是這樣用很容易就會到達極限。對待復雜的事情,一個更容易的辦法是把責任分給幾個不同的實體,每個實體負責一小塊它們自己要干的事情。

為什么要可測?

對于已經對單元測試感恩戴德的程序員來說,他們一定不會問這個問題。雖然單元測試讓他們的app在添加某個新功能時崩潰掉,但至少這時崩潰比在用戶的機器上崩潰要好,如果在運行期查找修復這樣的錯誤,可能要花費長達一周的時間。

為什么要易用?

小知識:最好的代碼是什么?是從來沒寫過的代碼。因為代碼越少Bug越少呀哈哈哈(好冷...)。不過這也說明,程序員不是因為懶才去琢磨怎么寫更好的代碼的。而且記得永遠不要忽略維護代碼的成本。
<br />

MV(X)基礎

當前我們有很多設計模式可以選擇:

  • MVC
  • MVP
  • MVVM
  • VIPER

前三個都是假設把app的各種實體歸為三類:

  • 模型:負責數據層,處理和提供數據。比如“Person”和“PersonDataProvider”。
  • 視圖:負責展示層(GUI)。比如iOS環境下各種以“UI”開頭的類。
  • 控制器/表達者/視圖模型:模型與視圖間的溝通橋梁,一般負責根據用戶的行為修改模型,再根據模型的變化更新視圖。

這種分類使得我們可以:

  • 更好的理解它們
  • 復用它們
  • 獨立地測試它們

現在先從MV(X)開始介紹,之后是VIPER。
<br />

MVC — 從前的它是怎樣的?

在說蘋果的MVC之前先來看一下傳統意義上的MVC:

在這里,視圖是無狀態的。只是在模型改變的時候,控制器將視圖做一個簡單呈現。就好像你按下一個網頁鏈接,然后網頁被刷新一樣。雖然在iOS應用中實現這種傳統的MVC是可能的,但是并沒有太大意義—因為三個部分耦合得過于緊密,每個部分都知道另兩個部分的存在。這極大地降低了它們的復用性,這是我們在應用中不想看到的。因此,我們這里就不為它舉例了。

傳統MVC模式在iOS開發中不適用。

<br />

蘋果的MVC—美好的理想

控制器是模型和視圖的中介,而模型和視圖互相不知道彼此的存在。最不具復用性的是控制器,不過這一點我們可以接受,因為我們必須有這樣一個地方來存放不能放在模型里的復雜邏輯。
理論上看起來非常直接,但是你會覺得有點不對勁。為什么呢?你有時可能聽到人們戲稱MVC為巨無霸視圖控制器(Massive View Controller)。而且,“為視圖控制器減負”成為了iOS開發者的一個重要話題。看起來蘋果只是把傳統MVC模式改進了一小下,為什么會發生這樣的情況呢?
<br />

蘋果的MVC — 殘酷的現實

Cocoa MVC模式促使你把視圖控制器越寫越大,因為在視圖的生命周期中它們是如此的水乳交融密不可分。即使你可以把一些邏輯和數據轉換卸載給模型,當你想卸載給視圖一點東西的時候,你會發現似乎沒有什么可以做的,大多數情況下視圖負責傳遞行為給控制器。所以最終視圖控制器變成了一個全世界的數據源和代理大管家,還經常負責分發網絡請求以及各種亂七八糟的操作。你肯定寫過無數次這樣的代碼:

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

這里的cell就是視圖,它是直接由數據配置的,MVC原則遭到了破壞,但是人們一直都這樣寫而且并不覺得有什么不對。如果你嚴格遵守MVC,你應該通過控制器來配置cell,而不是把模型傳給視圖,這樣做會讓你的控制器寫得更加冗長。

所以說,Cocoa MVC被戲稱為巨無霸試圖控制器(Massive View Controller)是不無道理的。

當遇到單元測試的時候,問題就更加明顯了。鑒于視圖和控制器是高度耦合的,你很難去做測試。因為你必須非常小心地去把復雜邏輯和視圖顯示的代碼分開,來模擬視圖的生命周期。

舉個栗子:


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集成可以在展示視圖控制器的時候進行。

這段代碼看起來可測性不是太好,對吧?我們可以把創建greeting移動到一個新GreetingModel類里然后單獨測試它,但是我們測試不了任何的展示相關的邏輯。因為這需要在GreetingViewController里直接調用UIView相關的方法(viewDidLoad, didTapButton),這可能會引起加載所有的view,而這不是單元測試的初衷。
事實上,在一個模擬器(比如iPhone4S)上加載測試UIView并不能保證它在別的設備上也好用,所以我建議,在模擬器沒有運行你的app的情況下,把“Host Application”從你的單元測試配置目標中移除后再進行測試。

視圖和控制器之間的交互在單元測試中并不能得到有效的測試。

這樣看來,Cocoa MVC似乎是個不怎么樣的模式。現在讓我們從剛才提到的好的設計模式的三個特點的角度來考察一下它:

  • 分配 — 視圖和模型是分開的,但是視圖和控制器是耦合的。
  • 可測性 — 鑒于分配實現得不好,你恐怕只能測試你的模型。
  • 易用性 — 對比其他模式來說,代碼量是最少的。并且,大家都很熟悉它,即便是不太有經驗的程序員也可以輕松維護它。

如果你沒有太多時間來打磨精修你的結構,或者你覺得對于你的工程規模,其他的模式維護成本過高,那么你應該選擇Cocoa MVC。

就開發速度而言,Cocoa MVC是最好的架構模式。

<br />

MVP — 實現Cocoa MVC 的理想


是不是看上去和蘋果MVC一模一樣?對沒錯就是的。它的名字叫做MVP(Passive View variant)所以這是說蘋果的MVC實際上是MVP嗎?并不是。不同于MVC的視圖和控制器的耦合,在MVP中的中介—表達者(Presenter),和試圖控制器的生命周期沒有任何關系,并且視圖可以輕易地被模擬,因此提出者里不用寫任何和布局(layout)有關的代碼,它負責視圖的數據和狀態更新。
如果我告訴你,UIViewController其實就是View,你會不會爆炸?


對于MVP,UIViewController的子類實際上是視圖而不是表達者。不要小看這個區別,它極大提升了可測性,雖然是以犧牲一定的開發速度為代價,因為你需要手動管理數據和時間之間的關系,就像下面這個例子一樣:

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
對于集成部分的重要說明

MVP是第一個顯現出集成問題的模式,因為這里有三個互相分離的層。我們不想讓視圖知道模型的存在,所以不能在視圖控制器(即視圖)里進行集成,只能在其他地方做。比如說,我們可以做一個整個app范圍內通用的“路由器”服務,來負責集成以及View-to-View之間的交流和表達。集成的問題不是只有MVP有,在之后介紹的所有模式里都會有這個問題。

現在看一下MVP的特點:

  • 分配 — 我們把大部分事情分給了表達者和模型去做,視圖則非常簡單。
  • 可測性 — 非常棒,因為視圖的設計很簡單,我們可以去測試大部分的邏輯。
  • 易用性 — 在上面這個已經極其簡單的例子中,代碼量也達到了MVC模式的兩倍。但是MVP的思想表達的很清楚。

iOS中的MVP代表了很好的可測性和長長的代碼。

<br />

MVP — 加上數據綁定

還有另一種別有風味的MVP—監控控制器MVP。與上面相比的變化包括:直接將視圖和模型進行綁定,而表達者(即監控控制器)依然負責處理視圖的行為,并且可以改變視圖。

但是我們也已經提到,這樣模糊的責任劃分是不對的,就像把視圖和模型耦合起來一樣,這和Cocoa桌面開發的情形很類似。
我剛才沒有為傳統MVC寫例子。同樣對于這個模式我也看不出舉例的意義何在。
<br />

MVVM — MV(X)家族最新最先進的成員

MVVM是MV(X)家族的最新成員,我們希望它的出現可以解決之前MV(X)的一些問題。
理論上MVVM(Model-View-ViewModel)模型看上去棒極了。視圖和模型我們都很熟悉了,這里的媒介改由View Model充當。


它和MVP很相似:

  • MVVM也把view controller當做視圖。
  • 視圖和模型之間沒有耦合。

另外,它也像監控MVP那樣做綁定,但是這次不是在視圖和模型之間,是在視圖和視圖模型之間。

所以iOS里的視圖模型到底是什么呀?大概來說,它是UIKit對視圖極其狀態的獨立的代表。視圖模型促使模型的更新,并且由于視圖和視圖模型之間有綁定,視圖也會隨之更新。

關于綁定

之前在MVP部分我簡要提到綁定了,這里我們再繼續討論一下。
綁定是在OS X的開發中出現的,但是iOS的工具盒里并沒有。雖然我們有KVO和通知機制,但是沒有綁定方便。
那么既然我們沒法自己寫,我們有兩個選擇:

  • 基于KVO的庫,比如RZDataBinding或SwiftBond
  • 全方位的功能反應性編程大野獸(…)比如ReactiveCocoa,RxSwift或者PromiseKit

事實上,現在如果你聽到“MVVM”,你就會想到ReativeCocoa,反之亦然。雖然用簡單的綁定實現MVVM是可行的,ReactiveCocoa是讓你實現MVVM大部分功能的首選。
然而有一個關于反應性框架的事實是,它們的強大功能需要被有責任有能力地使用。使用“反應性”是很容易造成混亂的,如果哪個地方搞錯了,你可能需要花非常長的時間debug。看一下下面的調用棧:


在我們簡單的例子中,FRF框架或者KVO都屬于殺雞用牛刀了。我們直接通過調用showGreeting方法和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
}
// Assembling of MVVM
let model = Person(firstName: "David", lastName: "Blaine")
let viewModel = GreetingViewModel(person: model)
let view = GreetingViewController()
view.viewModel = viewModel

又到了三特點分級打分時間:

  • 分配—在我們的小例子中不明顯,事實上在MVVM中,視圖比MVP中的視圖有更多的責任擔當。因為MVVM的視圖,它的狀態更新是由視圖模型設置綁定實現的,而MVP中的視圖只是把事件傳遞給表達者,自己不做更新。
  • 可測性—視圖模型不知道視圖,這使得我們可以方便地檢測它。視圖也可以被檢測,不過它是依賴于UIKit的,也許你會想跳過這一步。
  • 易用性—在我們的例子中它與MVP擁有相同的代碼量,但是在實際的app中,MVP的代碼量更多,因為你需要手動把事件從視圖傳遞到表達者,而MVVM可以用捆綁做到。

MVVM很有吸引力,因為它結合了之前模型的優點,并且不需要為視圖的更新增加冗長的代碼,并且可測性也還不錯。

<br />

VIPER — 像搭樂高玩具一樣設計iOS應用

VIPER是我們的最后一位選手,它不是MV(X)的成員所以更有趣喔。
你現在應該明白責任的明確劃分的好處了。VIPER在劃分責任上更上一層樓,現在我們有五層了:


來認識一下這些小伙伴:

  • 交互器(Interactor)— 包含于數據(實體)或網絡有關的邏輯,比如新建一個實體實例或者從服務器獲取他們。實現這種目的時你可能需要一些不屬于VIPER模塊的Services或者Managers,它們作為外在的依賴者出現。
  • 表達者(Presenter)— 包含與UI相關(但與UIKit獨立)的邏輯,調用交互器上的方法。
  • 實體(Entities)— 單純的數據對象,但是并非數據獲取層,因為那是交互器的功能。
  • 路由器(Router)-- 負責VIPER模塊間的連接。

與MV(X)相比,我們可以看到責任分配上的幾點不同:

  • 模型(數據交互)邏輯放到了交互器上,實體是單純的數據結構。
  • 表達者只有展示UI的責任,而沒有交換數據的功能。
  • VIPER是第一個清晰劃分出導航責任的,這部分由路由器完成。

用合適的方法完成路由轉發是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
    }
    
    // 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

最后一次三特點評分時間啦:

  • 分配—VIPER無疑是分配的冠軍。
  • 可測性—分配好嘛,當然可測性也好啦。
  • 易用性 — 以上兩點的優異表現,背后是易于維護性的犧牲。你需要寫很多分工明確的類和接口。
那么現在問題來了,到底跟樂高有什么關系?

當你用VIPER的時候,你可能感覺就像用樂高積木搭帝國大廈,這是就要注意了,你大概做的有問題。也許現在使用VIPER對你的工程來說太早了,也許你可以考慮用更簡單的模式。有些人忽略這些一味蠻干,我想他們以為即便現在的維護和開發成本很高,他們的應用在未來會受益于VIPER模式。如果你也這么覺得,我建議你去試試Generamba—一個建立VIPER骨架的工具。然而我覺得這就像你本可以拿彈弓去打麻雀,你卻非要用帶自動瞄準系統的玉米加農炮。
<br />

結論

我們介紹了幾個架構模型,我希望看完以后你之前的一些困惑得到了解答。你也一定發現了沒有全能的最優架構,選擇架構是一個在不同需求間找平衡的過程。
因此,在一個應用中混合使用不同的架構是很正常的。比如:你從MVC開始,然后你發現某個界面用MVC不夠高效,于是采用了MVVM,但只是對這個特定的界面。如果其他的界面用MVC用得好好的,沒有必要把它們也重構成MVVC,因為它們可以很好地共存。

我們追求簡單的做法,但不是為了簡單而簡單。 — 阿爾伯特.愛因斯坦

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

推薦閱讀更多精彩內容