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

421728768 8年前

来自: 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