美文网首页闻道丶技能(iOS)1 小知识点自鉴
iOS 的架构模式(揭秘 MVC,MVP,MVVM 和 VIPE

iOS 的架构模式(揭秘 MVC,MVP,MVVM 和 VIPE

作者: zyl04401 | 来源:发表于2016-04-18 02:44 被阅读2492次

    序言

    之前看了一篇国外大牛Bohdan Orlov写的关于 iOS 架构模式的文章,内容涉及目前 iOS 端诸多主流的模式,个人感觉文章写的很不错,收获匪浅,希望能够通过翻译原文的方式更好的体会一下,也分享给更多的人参考。原文地址在这里,并附上相关PPT,浏览原文可能需要科学上网。

    正文

    在 iOS 开发中使用 MVC 是否感觉很怪异?对 MVVM 感到有疑问?听说过 VIPER,但是又不确定它是否有价值?继续阅读本文,你将会找到这些问题的答案,如果没有找到满意的答案,请在评论中随便吐槽吧。本文将帮助你建立起关于 iOS 端架构模式的知识体系。我们先来简要地回顾一些主流的架构,并且从理论和一些小例子的实践上进行比较。

    注意:
    学习设计模式是进阶阶段,因此在阅读本文之前,假设你已有一定的基础,不会再询问如下的问题:
    1、谁应该持有网络请求,Model 还是 Controller?
    2、如何给View的ViewModel传递Model?
    3、谁能创建一个VIPER模块:Router 还是 Presenter?

    为什么要关心选择什么样的架构

    如果不关心架构,想象某天你调试一个巨大的类,里面有着数十个不同关联东西,你会发现几乎不可能定位问题点并修复bug。当然,你也很难去随心所欲地使用这个类,因为不了解类其中的一些重要细节。如果你在项目中已经遇到了这种场景,它可能是像这样的:

    1、这个类是UIViewController的子类
    2、数据直接存储在UIViewController中
    3、视图View没有任何操作
    4、Model的数据结构设计很糟糕
    5、没有单元测试覆盖

    即使你遵循了苹果指导意见并实现了苹果的 MVC 模式,这种情况还是可能会发生,不必觉得很难过。苹果的 MVC 有点问题,我们回头再说这件事情。让我们来定义一个好架构应该有的特征:

    1、严格划分,均衡分配实体间的角色和职责
    2、可测性通常是第一特性(不要担心:好架构一定具有可测性)
    3、便于使用,且维护成本低

    为什么要划分

    当试图了解程序如何运行时,角色和职责划分能够让我们保持思路清晰。如果你的开发能力越强,你就越能理解复杂的事物。但是这种能力并不是线性增长的,会很快达到极限。因此降低复杂性的最简单办法是遵循单一责任原则,划分多个实体之间的职责。

    为什么要可测性

    对于那些由于添加新特性,或者一些正在重构中的错综复杂的类来说,开发人员应该感激出现失败的单元测试,因为这些失败的单元测试可以帮助开发人员尽快定位运行中出现的bug,而这些bug可能出现在用户的设备上,甚至需要花费数周才能修复。

    为什么要易用

    这不需要回答,但值得一提的是,最好的代码就是不用写代码,因此写的越少越不容易出错。这意味着想写少量代码的想法不仅仅是因为开发者的懒惰,而且你也不应当被一个更灵巧的解决方案所蒙蔽,而忽略了维护它的成本。

    MV(X)系列导论

    现在当我们要做架构设计时有很多种模式选择:

    前三者采用的都是把App中实体划分成3类:

    • Models - 负责持有数据,进行数据处理的数据访问层。设想一下PersonPersonDataProvider类。
    • Views - 负责数据展现层(Graphical User Interface),在iOS端可认为所有以UI前缀的类。
    • Controller/Presenter/ViewModel - 负责协调处理ModelsViews之间的交互。

    通常用户操作视图会触发数据更新,数据的变更又会引起视图更新。这样的划分实体能让我们:

    • 更好的理解他们是如何工作的
    • 复用他们(通常可复用的是ViewsModels
    • 单独测试他们

    让我们开始学习MV(X)模式,稍后再说VIPER

    一、MVC(Model-View-Controller)

    在讨论Apple版本的MVC之前,我们先来看看传统的MVC

    Traditional MVC
    图示中,视图Views是无状态的,它只是当数据Models发生变化时,通过Controller控制简单地展现一下。设想当你点击网页上某个跳转链接时,整个网页就会重新加载。虽然在iOS应用程序中这种传统的MVC很容易实现,但这是没有意义的,因为架构上3类实体紧密的耦合在一起,每一类实体都要和其他两类产生关联,这会大大降低代码的可复用性。这不会是你想要的架构,由于以上原因,我们就不写这种MVC的典型例子了。

    传统的MVC不适合当前的iOS开发工作

    Apple版的MVC

    Apple期望的Cocoa MVC

    Cocoa MVC
    控制器Controller是视图Views和数据Models之间的中介,它们之间不需要有关联。可复用性最低的控制器Controller,通常是可以接受的,因为我们必须有一个地方来放置那些不适合放在数据Models中的所有复杂业务逻辑。理论上,它看上去非常简单明了,你是不是感觉到有什么问题?甚至听到过有人叫 MVC 为重控制器模式。此外,对于 iOS 开发者来说,给控制器减轻负担已经成为一个重要的话题。为什么苹果会采用仅仅改进过一点点的传统 MVC 模式呢?实际上的Realistic Cocoa MVC
    Realistic Cocoa MVC
    Cocoa MVC鼓励你写重控制器是因为它们在Views的生命周期中相互依赖,以至于很难将它们分开。虽然你可能有办法把一些业务逻辑和数据转模型的工作放到Models中,但是对于分摊到Views上的工作却没有什么办法,大多数情况下,Views的所有功能就是给控制器Controller发送操作事件,而Controller最终会成为你可以想到所有东西的代理或者数据源,比如通常会负责发送或者取消网络请求等等。你经常会看到这样的代码:
    var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell
    userCell.configureWithUser(user)
    

    cell 作为一个视图Views直接通过Models进行配置,MVC 的原则被违反了,但这种情况一直在发生,大家也没觉得有什么错。如果你严格的遵守 MVC,那么你就需要通过Controller对 cell 进行配置,并且不把Models传进Views中,然而这将会更进一步地增加Controller的规模。

    Cocoa MVC称作重控制器模式还是有一定道理的。

    问题直到需要进行单元测试了才会暴露出来(希望你的项目也一样)。由于ControllerViews紧紧的耦合在一起,单元测试变得很困难,因为你将不得不非常有想象力的去模拟Views的生命周期,写Controller测试代码时也必须尽可能把业务逻辑代码同Views的布局代码分离开。让我们来看一个简单的playground例子:

    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类中单独测试它。但是在不直接调用视图UIView相关方法(viewDidLoaddidTapButton这些方法可能会加载所有视图)的前提下,我们还是无法测试GreetingViewController内部任意的展现逻辑(虽然这个例子没有多少逻辑),这不利于单元测试。实际上,在一个模拟器(例如:iPhone 4S)上的测试并不能够保证在其他设备(例如:iPad)上也能运行良好。因此建议在单元测试中删除Host Application的配置,并且测试用例不要运行在模拟器上。

    视图和控制器之间的交互并不是真正的单元测试

    综上所述,Cocoa MVC似乎是一个相当糟糕的模式。让我们用文章开头提到的好架构特征来对它进行一个评估:

    • 划分 - ViewModel确实是分离了,但是ViewController还是紧紧地耦合在一起。
    • 可测试性 - 由于划分的不好,你可能只能测试你的Model
    • 易用性 - 相比于其他模式代码量最小,此外门槛低,每个人都能熟练掌握,即使不是一个非常有经验的开发者也能进行维护。

    如果对于你的小项目,不打算投入很多时间去设计架构,也不打算投入太多成本去维护,那么Cocoa MVC是你要选择的模式。

    在开发速度上,Cocoa MVC是最好的架构模式。

    二、MVP(Model-View-Presenter)

    Cocoa MVC的演变

    Passive View variant of MVP
    看上去是不是很像Cocoa MVC?的确很像,只是名叫MVP(Passive View Variant)。稍等。。。这是否意味着MVP的实质就是Cocoa MVC呢?当然不是,因为你回想一下ViewController紧紧耦合在一起的位置,在MVP中是Presenter ,它与视图控制器的生命周期没有任何关联,并且由于没有任何布局的代码,很容易模拟视图View。它的职责是更新View中的数据和状态。

    如果我告诉你UIViewController就是视图,会怎么样

    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()
    }
    
    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是第一个揭示了实际上由3个独立分层会存在组合问题的模式。既然我们并不希望ViewModel耦合,在视图控制器中(实际上是View)组装它们就是不正确的,因此我们需要在其他地方处理。例如,我们可以让App范围内的路由服务来负责处理View与View之间的展现。这个问题不仅MVP中存在,后面将要介绍的所有模式中也都存在。我们来看看MVP的特征:

    • 划分 - 大部分职责都被划分给了PresenterModelView没有任何职责(例子中的Model也没有职责)。
    • 可测试性 - 很好,我们可以测试大部门业务逻辑,因为View无职责。
    • 易用性 - 在我们那个的不切实际的例子中,代码量比MVC翻了一倍,但同时,MVP的设计思路非常清晰。

    在iOS开发中使用MVP模式意味着良好的可测性和很多的代码量。

    基于绑定和监控

    还有另外一种形式的MVP模式 — 带监控器的MVP。这种模式的特点包括直接绑定ViewModel,同时Presenter(监控器)仍然控制着View上的操作事件,并能改变View的展现。

    Supervising Presenter variant of the MVP
    但正如我们之前认识到的,模糊不清的职责分配是不好的设计,ViewModel也紧紧的耦合在一起。这种模式跟Cocoa桌面端程序开发相似。和传统的MVC模式一样,对于有缺陷的架构,我认为没有必要再举例。
    三、MVVM(Model-View-ViewModel)

    MVVM是最新的MV(X)系列架构,我们希望它在设计之初就已经考虑到之前的MV(X)系列所面临的问题。从理论上来看,Model-View-ViewModel看起来不错。ViewModel我们已经很熟悉了,但中间层换成了ViewModel

    MVVM
    它和MVP模式很像:
    • 视图控制器划分成View
    • ViewModel之间没有紧密的耦合

    此外,数据绑定的概念很像带监控器的MVP,不同的是这次绑定的是ViewViewModel,而不是ViewModel。那么在实际的iOS开发中ViewModel是什么?从根本上来说,它是独立于UIKit能够展现你的View和状态。ViewModel可以调用Model来改变数据,也可以通过数据变更来更新自己,因为ViewViewModel进行了绑定,相应地也就能同步更新View

    绑定

    在介绍MVP部分我简要地提到绑定的概念,但我们还是在这里讨论一下它。绑定来源于OS X开发环境,在iOS开发中没有。当然我们可以使用KVO和消息通知机制,但都不如绑定方便。因此,如果我们不想自己实现一套绑定机制,有两种选择:

    实际上,当你听到MVVM就会联想到ReactiveCocoa,反之亦然。虽然使用ReactiveCocoa框架可能是你很容易建立起基于绑定的MVVM,并且发挥出它的最大价值,但响应式框架有一个痛苦的现实:能力越大,责任也就越大。当你使用响应式框架的时候很容易就搞得乱七八糟,换句话说,如果出现bug,你将会花费大量的时间去调试bug,看看下面的调用堆栈图。

    Reactive Call Stack

    在我们的简单例子中,无论是使用函数响应式框架,还是KVO都有点大材小用。我们换另外的方式,通过调用showGreeting方法来请求View Model更新View,使用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的特征:

    • 划分 - 在我们这个小例子中看到的不是很清晰,但实际上,MVVM中的View承担了比MVP要多的职责。首先它需要绑定ViewModel来更新状态,其次它需要传递所有的事件消息而不需要更新事件的提供者。
    • 可测试性 - ViewModel并不持有View,这让我们很容易测试它。View也可以测试,但它依赖UIKit通常会忽略掉。
    • 易用性 - 代码量和MVP一样多,但真实的App开发中如果采用绑定机制,去替换那些传递事件和手动更新的代码,会减少很多代码量。

    MVVM是非常吸引人的,因为它结合了前面提及的几种模式的优点,此外使用绑定机制不需要编写额外的视图更新代码,并且保持了良好的可测试性。

    四、VIPER(View-Interactor-Presenter-Entity-Routing)

    从搭积木中领悟的iOS设计

    VIPER是本文最后一个介绍的架构模式,它很有趣,不属于MV(X) 系列的扩展。到目前为止,你必须意识到一个好的设计一定有细粒度的职责划分。VIPER从另一个不同的角度进行了职责划分,这次我们分为5层:

    VIPER
    • 交互器:包含与数据(实体)或网络相关的业务逻辑,比如从服务器获取一些新的数据实体,为了这些目的,你会使用一些ServicesManagers,它们并不被认为属于VIPER中的一部分,更确切地说它们是一种额外的依赖。
    • 展示器:包含一些与UI相关(UIKit除外)的业务逻辑,通过交互器调用方法。
    • 实体:纯粹的数据对象,不包含数据访问层,因为这是交互器的职责。
    • 路由器:负责VIPER模块之间的切换。从根本上说,粒度划分方式,VIPER模块可以用来设计一个场景的功能,也可以用来设计应用中的一个完整用户故事—比如身份验证,是由一个场景或者若干场景组成,应该用多大的积木块来搭乐高玩具,完全取决于你。

    MV(X)系列对比,我们会发现在职责划分上有一些不同点:

    • Model — 数据交互逻辑被转移到了交互器Interactor中,Entities只有纯粹的数据结构。
    • Controller/Presenter/ViewModel中的UI展现职责转移到了交互器Interactor中,但它没有更改数据的能力。
    • VIPER是第一个明确提出地址导航职责应该由路由器Router来解决。

    在iOS应用中找到一种合适的路由方式是一个挑战,MV(X)系列模式都没有定位到这个问题。

    下面的VIPER例子中没有涉及到模块之间的路由或交互,当然在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在职责划分上是最好的。
    • 可测试性 - 毫无悬念,好的职责划分必然有好的可测性。
    • 易用性 - 由于上述两个特征你就可以猜测到代码维护性成本很高,你不得不编写大量的接口类来完成很小的职责。

    总结

    我们已经从头到尾地了解了几种架构模式,希望你能从中找到那些曾经困扰你很久的问题的答案。但我毫不怀疑,你已经意识到了没有什么银色子弹,选择什么样的架构设计是特定场景下权衡各种因素之后的结果。因此,在同一个app中就会出现混合架构设计。比如:一开始使用MVC,然后你发现有一些特殊场景如果使用MVC将会难以维护,这时你可以仅对这个场景使用MVVM模式,没必要去重构那些MVC架构执行的很好的模块。MV(X)系列是互相兼容的。

    Make everything as simple as possible, but not simpler. -- Albert Einstein

    相关文章

      网友评论

      • 小凡凡520:let person: Person

        你确定????
      • f567ca3b98c0:“Controller/Presenter/ViewModel中的UI展现职责转移到了交互器Interactor中,但它没有更改数据的能力”

        这里有错误,应该是转移到展示器Presenter中,Presenter没有直接和数据交互的能力

      • lesmiserables0:就喜欢楼主这样胸怀宽广的人,那我就随便问了啊。
        1、谁应该持有网络请求,Model 还是 Controller?
        2、如何给View的ViewModel传递Model?
        3、谁能创建一个VIPER模块:Router 还是 Presenter?
        解决这三个问题,应该学习哪方面的知识,或者说搜索哪些关键字。目前只知道,M,V,C.
        ViewModel, VIPER,Router,Presenter 这些单词没怎么听过都。

      本文标题:iOS 的架构模式(揭秘 MVC,MVP,MVVM 和 VIPE

      本文链接:https://www.haomeiwen.com/subject/vadolttx.html