讲一下MVC和MVVM,MVP

作者: heweiming 分类: 未分类 发布时间: 2017-04-21 21:18

原文地址在这里,浏览原文可能需要科学上网。

在wiki给出了一个很详细的解析

(1).MVC

(2).MVVM

(3).MVP

前面三种把 App 中的所有实体都分成下面三种类型:

  • Models-拥有数据或者说是操作数据的数据访问层(DAL),可以联想一下 Person 类或者 PersonDataProvider 类。
  • Views-对应展现层(GUI),对于 iOS 来说可以想一下所有的以‘UI’作为前缀的东西。
  • Controller/Presenter/ViewModel -Model 和 View 的粘合剂或中转站,通常来说,用来处理作为用户在 View 上操作的反馈来更改 Model,或者因为 Model 的改变来更新 View 上的展示。
  • 把实体进行划分能够让我们:

  • 理解的更好(我们已经知道了)
  • 重用(通常都是 View 和 Model 比较适合重用)
  • 独立进行测试
  • 但是苹果的MVC跟传统的MVC又有点不一样

    MVC

    MVC原来是什么样子的

    在讨论苹果版本的 MVC 之前我们先来看看传统的 MVC 是什么样的?

    Tranditional MVC

    这样的话,View 是没有归属的(stateless),只要 Model 变化了,就会被Controller 提供(渲染?)。想想 web 网页,只要你点击链接到别的地方的网页,网页就会彻底重新加载。虽然这种 MVC 结构在 iOS 应用中很容易实现,但是因为在结构上,这三种实体是紧紧耦合在一起的,每一种实体都和其他两种实体有联系,所以这种结构也起不到什么作用,并且还戏剧化的降低了其可复用性,所以这种结构不会是想要在你的应用中使用的。由于以上原因,我们在这里就不写这种 MVC 的例子了。

    传统的MVC结构看起来并不适合现在的iOS开发工作

    苹果的MVC

    期望(Expectation)

    控制器来作为视图和模型中间的中转站,这样视图和模型之间互相就没有直接联系了。这样的话,控制器的可复用性就变得最低,但是这个对于我们来说也是可接受的,因为我们需要有一个地方来放那些不方便放在模型中的复杂业务逻辑。

    Cocoa MVC

    理论上来讲,这种结构看起来非常直接,但是是不是觉得有点不对劲?甚至听到过有人叫 MVC 为重控制器模式。此外,对于 iOS 开发者来说,给控制器减轻负担已经成为一个重要的话题。为什么苹果会采用仅仅改进过一点点的传统 MVC 模式呢?

    残酷的现实(Reality)

    Realisttic Cocoa MVC

    Cocoa MVC 鼓励你写重控制器是因为它们互相之间在视图的生命周期中互相牵扯,以至于很难将它们分开。虽然你也可能有一些办法把一些业务逻辑和数据转模型的工作放到 Model 中来,但是对于把负担分摊到 View 上却没有什么办法,大多数情况中,View 的所有功能就是给 Controller 发送动作(action),而 Controller 则会成为所有东西的代理或者数据源,并且通常会发送或者取消网络请求等等等等,你可以尽情想象,iOS开发里面不难看到会有1000行代码的Controller。

    你应该经常可以看到这种代码:

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

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

    所以MVC被称为重Controller,不无道理

    有些人或许会说,管什么设计模式,拿起键盘就是干。但是我只能说,博主曾经也是这样子认为,并且也写过上千行代码的Controller。虽然需求实现了,但是需求改一下,原来的东西不能用了,但是Controller的网络请求,或者有些交互式还可以用的。那么咋整?与其花时间把这些把代码搬来搬去,倒不如直接把东西一次性想好。我参加过一个外国的项目,他们的管理模式的确是很先进(我估计是国内大厂才会有这样子的工作模式)。他们把东西拆得非常非常细,用的Clean-Swift这个设计模式。一开始,我觉得这样子的工作模式非常苦恼,非常麻烦。但是随着逐渐的深入,会发现,这样子是很有必要的。他们的UI经常做修改,但是我修改的代价可能只是微调一下,毕竟所有东西都模块化做的很好,我不用想我改了这里会全世界都出问题。

    如果偏要说我刚才说的问题是个例的话,那我只能说接下来问题就来了。

    单元测试这个基本上已经是项目上线落地之前都要做的事情。如果遵循MVC的设计模式,你的控制器跟试图处于高耦合状态。要不是你们公司有测试,而且测试妹子是个经验丰富到能把你的代码全部读一遍然后都能读懂的程度,要不你需要非常有想象能力才能把这些视图跟它们的生命周期模拟出来。

    代码栗子来一波

    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 的相关方法(viewDidLoad, didTapButton)的前提下,没有办法测试所有的业务逻辑,这些方法有可能加载所有 View,这个对于单元测试来讲是不利的。

    事实上,在一个模拟器上(例如:iPhone 4S)测试 UIViews 无法保证在其他设备上(例如:iPad)也运行良好,所以我建议在你的 Unit Test target 配置中去掉Host Application,让你的测试用例不要在程序跑在模拟器上的时候运行。

    View 和 Controller 之间的交互在单元测试中是无法被真正测试的。

    从上面来看,Cocoa MVC 是一个非常不好的选择,但是让我们就这文章开头提到那些特征而言来对它进行一个评估:

  • 划分(Distribution)--View 和 Model 确实是分开了,但是View 和 Controller 紧紧的联系在一起
  • 可测试性--由于功能划分的不好,你可能只能测试你的 Model
  • 易用性--与其他模式相比代码量最小,另外,每个人都对他很熟悉,即使是一个不是非常有经验的开发者也能进行维护
  • 如果你不希望花费特别多的时间在你的架构上,并且你觉得对于你的小项目来说,更高的维护成本并不值得的话,Cocoa MVC 是你要选择的模式。

    就开发速度而言,Cocoa MVC是最好的框架模型

    MVP

    Cocoa MVC’s promises delivered

    Passive View variant of MVP

    它看上去是不是非常像 Apple’s MVC ?是的,确实很像,并且叫做MVP(Passive View variant)。但是等一下,这意味着 Apple’s MVC 实质上就是 MVP 吗?不是的,还记得 View 是紧紧和 Controller 联系在一起的吧,而在 MVP 中,作为中转站的 Presenter 与视图控制器的生命周期没有任何关联,并且 View 很容易被模拟,所以在 Presenter 中没有任何页面布局的代码,但是 Presenter 有责任通过数据和状态来更新 View。

    如果我告诉你,UIViewController 是 UIView 会怎么样?

    就 MVP 而言,UIViewController的那些子类实际上是视图而不是 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
        }
    
        // 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
    

    关于组装(assembly)的重要说明

    MVP 有三个真正分开的层(three actually separate layers),所以可能会引起拼装的问题,MVP 是第一个披露有这种问题的架构模式。我们不希望 View 和 Model 进行联系,所以在当前的 view controller(其实就是 View) 中进行组装是不正确的,所以我们需要在别的地方做这件事情。例如,我们可以做一个app 范围(app-wide)内的路由服务,由它来处理组装的工作和视图到视图的呈现(presentation)。这个问题不仅仅是在 MVP 中有,下面介绍的所有架构模式都存在这个问题。

    我们来看一下 MVP 的特点:

  • 划分(distribution)--大部分的任务都被划分到 Presenter 和 Model 中,而 View不太灵活(例子中的 Model 也不太灵活)
  • 可测试性--非常出色,我们可以通过 View 来测试大部分的业务逻辑
  • 易用性--在我们那个不切实际的小例子里,MVP 的理念是非常清晰的,但是代码量是 MVC 模式的两倍
  • MVP 在 iOS 中使用意味着非常好的可测试和非常多的代码

    With Bindings and Hooters

    MVP 还有另外一种特色(的变种)--Supervising Controller MVP(SoC MVP)。这个变种包括了 View 和 Model 的的直接绑定,与此同时 Presenter(The Supervising Controller)仍然控制对 View 上操作的响应,并且能够改变 View (的展示)。

    Supervising Presenter variant of the MVP

    像前面我们认知的那样,模糊的职责划分以及 View 和 Model 的紧耦合是一件不好的事情,这个也跟 Cocoa 桌面端程序开发相似。

    跟传统的 MVC 一样,我对于有着致命错误的架构没什么兴趣写一个例子。

    MVVM

    最新的也是最好的MVX种类

    MVVM 是最新的 MV(X) 系列的架构,让我们希望它出现的时候已经考虑到了 MV(X) 以前遇到的那些问题。

    理论上来看,Model-View-ViewModel 的架构模式看起来不错,View 和 Model 对于我们来说都比较熟悉了,但是对于中转站这个角色是由 ViewModel 来充当。

    MVVM

    它和MVP非常像

  • MVVM 把 View Controller 作为 View
  • View 和 Model 之间没有紧耦合
  • 另外,MVVM 在数据绑定方面有点像 SoC 版本的 MVP,但是不是在 View 和 Model 之间进行绑定,MVVM 是在 View 和 ViewModel 之间进行绑定。

    那么,在 iOS 的开发场景中,什么是 ViewModel 呢?基本上来说,它是 View 和 View 状态的独立于 UIKit 外的一个呈现,ViewModel 调用 Model 中的的变化,根据 Model 的变化进行调整,并且通过 View 和 ViewModel 的绑定,同步调整 View。

    绑定

    虽然我简单的在 MVP 的部分提到过这个内容,但是我们还是要在这里对这个话题进行一下讨论。“绑定”来源于 OS X 开发,但是在 iOS 的工具中并没有这个。虽然我们有 KVO 和通知机制,但是都不如绑定来的方便。

    那么如果我们不想自己写一套的话,我们有两种选择:

  • 选一个基于 KVO 的绑定框架,如:RZDataBinding或者SwiftBond
  • 使用全量级的函数式响应编程的框架,如ReactiveCocoaRxSwift或者PromiseKit
  • 实际上,现在我们提起到 MVVM 就会想到 ReactiveCocoa,反之亦然。虽然,我们可以通过简单的绑定来使用 MVVM,但是 RAC(或者同类框架)能够让你更大程度的使用 MVVM。

    关于响应式的框架有一个问题,就像更大权利对应着更大的责任,使用响应式的变成,非常容易把事情搞得一团糟,换句话说如果你出了错误,可能会需要花非常多的时间去进行调试,看看下面一张调用堆栈图你就明白了:

    Reactive Call Stack

    在我们下面这个简单的例子中,无论是响应式编程的框架还是 KVO 都太重了,我们用另外一种方式进行替代:通过 showGreeting 方法以及利用 greetingDidChange 方法的闭包参数,直接调用 ViewModel。

    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 的几个特征进行一下评估:

  • 划分(distribution)--也许在我们的小例子中表现得不是太清楚,但是实际上 MVVM 的 View 比 MVP 的 View 要做的时期要多一些,因为,通过设置绑定,第一个 View 由 ViewModel 来更新状态,然而第二个只需要将所有事件传递到 Presenter 就行了,不需要更新它的状态
  • 可测试性-- ViewModel 并不持有 View,这让我们可以轻松的对它进行测试,View 也可以进行测试,但是它是依赖于 UIKit 的,你可能会忽略它
  • 易用性--在我们的例子里,MVVM 的代码量跟 MVP 差不多,但是在实际的 app 中,需要把事件从 View 传递到 Presenter ,并且要手动的更新 View,如果使用绑定的话,MVVM 将会瘦身不少
  • MVVM 非常具有吸引力,因为它结合了上述几种框架方法的好处,另外得益于 View 中的绑定机制,它也不需要额外的代码,并且可测试性也处在一个相当不错的层次。

    如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

    发表评论

    电子邮件地址不会被公开。 必填项已用*标注