iOS架構模式-揭秘MVC,MVP,MVVM和VIPER

421728768 8年前發布 | 36K 次閱讀 iOS開發 移動開發

來自: http://blog.csdn.net/cuibo1123/article/details/50681389


iOS架構模式

揭秘MVC,MVP,MVVM和VIPER

英文原文:https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52#.w3sovqjl3

作者:Bohdan Orlov

翻譯:http://blog.xoneday.com/

在IOS中使用MVC感覺很奇怪?對切換到MVVM存在疑慮?聽過VIPER,但是又不確定是否值得嘗試?

繼續閱讀,你會找到上面問題的答案,如果沒有你想要的答案,你可以去評論里罵我。

你將要開始學習一些有關ios架構模式的知識。我們將會簡單的回顧一些當前受歡迎的架構模式,并在原理上對他們進行比較,然后做一些小例子來實踐。如果你需要了解更多詳細信息,我也為你整理了一些鏈接。

學習設計模式可能會上癮,所以要小心:讀完這篇文章可能會給你帶來更多的問題,像這些

  • 誰應該做網絡請求:模型還是控制器?
  • 如何把一個模型傳遞到一個新的視圖?
  • 誰創建一個新的VIPER模塊:Router 還是 Presenter?

 


為什么要在乎架構?

因為如果你不這樣做,總有一天你要面對去調試幾十個巨大的類的工作,你會發現自己暫時無法查找和修復任何bug。你也很難再大腦里對這些類有一個總體的印象,所以,你會一直無法修復或完善一些重要的細節。如果你的程序已經這樣了,那非常可能:

  • 這個類是UIViewController的子類
  • 你的數據直接存儲在UIViewController中
  • 你的UIViews幾乎沒事情做
  • 你的模型就是一個簡單的數據結構
  • 沒有單元測試

即便你遵循了蘋果的指導,使用了Apple’s MVC,也可能會發生這樣的問題。不要灰心,Apple’s MVC也有一些不妥當的地方,我們稍后來討論這個問題。

我們來定義一下好的架構應該符合哪些特征:

  1. 角色實體之間的任務嚴格均衡分布;
  2. 可測試性(通常來自于上一條規則,在好的架構中很容易實現);
  3. 易于使用并且維護成本低;

為什么要分布

當我們要弄清楚一件事如何工作的時候,我們的大腦需要負載這件事情的復雜性。當然你越是成長,你的大腦也越能適應并理解更復雜的事物。但這種能力并不是不斷線性發展的,很快你就會遇到一個瓶頸。因此,要打敗復雜性最簡單的辦法就是讓每個實例負責單一任務(single responsibility principle)。

為什么要可測試

誰都不會懷疑單元測試為重構和添加新功能時遇到的問題帶來的好處。他也能讓開發人員發現很多運行時問題,如果這些問題等到在用戶設備上被發現,則修復并達到用戶至少需要一個星期(takes a week)。

為什么要易用

這似乎不需要答案,值得一提的是,最好的代碼就是沒有代碼。因此,更少的代碼,也代表更少的錯誤。寫更少代碼并不表示開發人員更懶惰,你不應該總是傾向于使用更完美的解決方案,而看不到他的維護成本。

MV(X) 要點

當涉及到架構模式時,我們有很多選擇:

先假設一個應用分為三個部分:

  • Models(模型) - 負責數據或操作數據的數據訪問層(data access layer),比如‘Person’或‘PersonDataProvider’ 類。
  • Views(視圖)-負責表示層(GUI),iOS一般用’UI’開頭
  • Controller(控制器)/Presenter(主持人)/ViewModel?(視圖模型) - Model和View中間的中介,一般負責對Model的變化做出反應,接受用戶操作并修改Model,更新View等。

實體的劃分使我們能夠:

  • 更好的了解(因為我們已經知道模塊是干什么的了)
  • 重用(主要適用于View和Model)
  • 獨立測試

讓我們先從mv(x)模式開始,然后在說VIPER。

MVC

如何使用它

在討論蘋果版本的MVC之前,我們先看看傳統的MVC(traditional one)

 

在這種情況下,View是無狀態的。一旦Model發生改變,它就通過Controller來進行簡單的呈現。想想網頁,一旦你按下某個鏈接,瀏覽器就重新加載新的頁面。雖然在ios中是可以實現傳統MVC的,但是它的意義不是太大-三個實體是緊耦合的,每個實體都對其他實體可見。這大大降低了模塊的可重用性-這就是為什么你的應用程序不使用MVC的原因。出于這個原因,我們就不寫MVC的例子了。

傳統的MVC似乎不適合用于IOS開發。

蘋果的MVC

期望

 

Controller是View和Model的中間人,讓它們彼此不可見。我們通常會構造一些最小可復用的Controller,但是一些比較棘手的不適合放在Model里的業務邏輯也會放在其中。

從理論上來看,appleMVC看起來很簡單。但是你總感覺有些不對?你一定聽到過有人構造了一個超大體積的Controller。此外,View Controller的卸載成為了ios開發者的一個重要問題。為什么會出現這種情況那?為什么蘋果要把傳統的MVC改進成這樣?(view controller offloading )

Apple’s MVC

現實

 

CocoaMVC鼓勵你寫超大體積的View Controllers,因為它跟視圖的生命周期非常相關,很難說它們是相互獨立的。盡管你可以分擔一部分業務邏輯和數據轉換工作到Model,但是當涉及到釋放工作的時候,你的選擇并不多。在大部分時間,視圖的唯一責任就是發送動作到控制器,視圖控制器最終成了一個代理,并且負責數據以及收發網絡請求相關的一切你能想到的任務。

這種代碼你見過多少次:

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

這個cell其實是一個視圖,并且直接用模型進行配置。那么這個違反了MVC準則,但是當你這樣使用的時候,并沒有覺得有什么不對。如果嚴格遵守MVC,則應該從視圖控制器配置視圖,而不是傳遞一個模型到視圖,但這樣會讓你的視圖控制器變得更大。

在CocoaMVC中,制造一個大體積的視圖控制器是合理的。

這個問題可能并不明顯,直到它涉及到單元測試(如果你的項目中包含單元測試(Unit Testing))。由于視圖跟控制器緊密結合,因此你想要測試視圖的生命周期必須用一些非常有創意的辦法,在以這種方法構造視圖控制器時,你的業務邏輯應該盡可能與視圖布局分離。

讓我們來看一個簡單的例子:

#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的裝配(代碼中的Assembling of MVC部分)可以在呈現視圖控制器時執行

但我們不能直接調用UIView的相關方法(viewDidLoad, didTapButton) ,這可能會導致無法測試GreetingViewController視圖的加載和表示層邏輯(盡管上面的例子中沒有太多這樣的邏輯),這不是好的單元測試。

事實上,在一個特定的模擬器上 (例如iPhone 4S)加載和測試UIView,并不能保證它在其他設備上(例ipad)也能很好的運行。所以我建議從你的單元測試配置中刪除“Host Application(宿主應用程序)”,并在沒有宿主應用的情況下進行測試。

視圖和控制器之間的相互作用在單元測試中是不可測試的(aren’t really testable with Unit Tests)。

這樣看來,Cocoa MVC似乎是一個很糟糕的設計模式。但我們用文章開頭的特點來評估它:

  • 分布 - 視圖和模型分離,但視圖跟控制器緊密耦合。
  • 可測試 - 由于分配不好,你只能測試你的模型。
  • 易用性 - 代碼涉及的模式很少,并且每個人都熟悉這種模式,因此即便是沒有經驗的開發人員,也很容易維護它。

如果你不準備在架構上投入更多時間,那么Cocoa MVC是你的首選模式,如果你覺得這種模式維護成本過高,那可能是你的項目設計過頭了。

從開發速度上來說,Cocoa MVC是最好的設計模式。

MVP

Cocoa MVC承諾交付

 

是否看起來很像Apple’s MVC?的確是這樣,并且它的名字是MVP(Passive View的變體),這是否意味著Apple’s MVC其實就是MVP?不,它不是,如果你記得,在Apple’s MVC中,視圖和控制器是緊密耦合的,而在MVP中,控制器是調解員、主持人,視圖控制器的生命周期與視圖無關,并且可以很容易的模仿一個視圖出來,因此Presenter可以沒有任何布局代碼,它只負責更新數據和視圖狀態。

 

如果我告訴你,視圖控制器就是視圖。

在MVP里,UIViewController的子類其實都是視圖,而不是Presenter,這種區分提供了更好的可測試性,但卻降低了開發速度,因為你必須手動綁定事件和數據,你可以看看下面的例子:

#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()
}

//Presenter
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)
    }
}

//view
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模式恰好由三個獨立的層組成。我們不希望視圖層了解模型,把模型呈現在視圖控制器(在這里就是視圖)里的裝配是不正確的。因此,我們必須在別的地方做這個工作。另外,我們可以使用應用級的路由服務來完成視圖-to-視圖的跳轉。這些問題不僅存在于MVP中,而是在所有模式中都需要注意。

讓我們來看看在MVP的特點:

  • 分布 - 我們劃分了Presenter和模型,以及非常簡單的視圖(視圖不對模型進行操作)。
  • 可測試 - 優秀,我們可以測試大部分的業務邏輯,由于視圖只負責展示。
  • 易用性 - 在我們非常簡單的例子中,相比MVC,代碼量幾乎翻倍。但MVP的思路很清晰。

MVP在iOS中具有手段高超的可測試性和大量的代碼。

MVP

綁定和警報

這是一個變種的MVP - ?the Supervising Controller MVP. 這種變體包括直接結合的視圖和模型,同時Presenter(The Supervising Controller)仍然處理來自視圖的操作并能夠改變視圖。

 

             Supervising Presenter variant of the MVP

但是,正如我們已經說過的,模糊的職責分離,視圖和模型的緊密耦合是不好的。工作原理類似于Cocoa桌面開發。

于傳統的VMC相比,我沒有看到這種架構的優勢。

MVVM

最新最大的一種MV(x)

MVVM是一種最新的MV(x),所以,我們希望它的出現解決了之前MV(x)面臨的問題。

從理論上講,Model-View-ViewModel看起來非常好。其中的視圖和模型都已經為我們所熟悉,其中的調解者,被稱為ViewModel。

 

它和MVP很相似:

  • 在MVVM中,將視圖控制器作為視圖。
  • 還有就是視圖和模型之間沒有緊密耦合。

此外,它有點像監控版本的MVP(Supervising Controller MVP),不過這一次不是綁定視圖和模型,而是在View Model中完成這個功能。

那么,View Model在ios中怎么實現那?它基本上與你的視圖以及UIKit無關。View Model 在調用更新模型的同時更新自己,而且由于我們有一個與View Model綁定的視圖,所以視圖也被相應的更新了。

綁定

在MVP的部分我們簡單提到過這個問題,這里我們討論一下。在OSX上系統已經提供了綁定工具,但是在IOS上卻沒有。當然,我們有KVO和通知,但他們不如綁定方便。

如果我們不希望自己實現這個功能,那么我們至少有以下兩種選擇:

實際上,現在如果你聽到“MVVM”,你可以認為就是ReactiveCocoa,反之亦然。雖然你可以使用簡單的綁定來實現MVVM,但大多數MVVM模式都直接使用了ReactiveCocoa(或其分支)。

有一個關于響應式框架(ReactiveCocoa)慘痛的道理:權利越大責任越大。使用響應式框架很容易把事情搞砸。換句話說,如果你在某個地方做錯了,你可能要花費大量的時間來調試程序,只要看看下面這個堆棧調用就能明白多麻煩了:

 

Reactive Debugging

在我們的簡單例子中,使用FRF框架或者KVO都顯得太重了。取而代之的是我們會明確的要求View Model使用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的視圖責任更多。第一所有狀態更新需要用View Model建立綁定,第二所有事件只是由Presenter轉發,并不自己更新。
  • 可測試-View Model對視圖一無所知,這讓我們很容易測試它。視圖也可是可測試的,但是它依賴于UIKit,你可能希望跳過它。
  • 易用性-在我們的例子中,其擁有的代碼與MVP似乎一樣多,但是在真正的程序中,你應該不會手動綁定事件或更新視圖。如果你使用綁定,MVVM的代碼量將會非常少。

MVVM是非常有吸引力的,因為它結合了上述方法的好處,此外,它不需要額外的代碼更新視圖,因為視圖的更新綁定在了視圖里。同時可測試性也不錯。

VIPER

樂高積木的經驗轉移到iOS應用中

VIPER 是我們最后的備選,它比較特別,因為它不是從MV(X)擴展出來的。

現在,你必須同意,細力度的責任劃分是非常好的。VIPER又一次迭代了責任劃分,這里,我們有五個層次。

 

  • Interactor - 包括數據實體、業務邏輯、網絡請求等,如創建新的實例,或者從服務器獲取實例。為達到這一目的而使用的一些服務或管理,不被視為VIPER的一部分,而是作為一些外部依賴。
  • Presenter - 包括UI相關(但不包括UIKit相關)的業務邏輯,調用了Interactor的方法。
  • Entities - 你的數據對象,而不是數據訪問層,因為那是Interactor的責任。
  • Router - 負責VIPER模塊之間的連接。

基本上,一個屏幕所展示的內容或者多個相關屏幕所組成的一個場景可以就是一個VIPER。

你想用你的樂高積木做什么?隨便你。

如果我們用MV(X)來進行比較,就可以看到責任劃分的不同之處:

  • Model(數據交互)的邏輯轉移到了Interactor,Entities只是簡單的數據結構。
  • 只是將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
    }

    // 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框架的工具。我個人覺得這就像是是用自動瞄準系統的火炮取代彈弓。

結論

我們已經了解了這幾種架構模式,我希望你能夠從中找到一些問題的答案。但是我相信你意識到了沒有萬能的架構,所以選擇架構模式是一個在特殊情況下權衡權重的問題。

因此,在同一應用程序中混合不同的架構模式是很自然的事情。例如:你已經開始使用MVC,然后你意識到一個特定的屏幕使用MVC太難實現和維護,切換到MVVM會變得簡單的多。但是為了這個特殊的屏幕,并不需要重構其它使用MVC能夠很好工作的屏幕,因為這兩種架構很容易兼容。

讓一切盡可能地簡潔明了,但又不能簡單地被簡化。 —?Albert Einstein

 本文由用戶 421728768 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!