[译]iOS架构模式——解密MVC、MVP、MVVM和VIPER

为避免撕逼,提早声明:本文纯属翻译,仅仅是为了学习,加上水平有限,见谅!react

【原文】https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52ios

使用MVC进行iOS开发感受到很怪异?在切换到MVVM的时候心存疑虑?据说过VIPER,可是不知道是否值得采用? 读下去,这篇文章将为你一一解惑。 若是你正打算组织一下在iOS环境下你掌握的架构模式知识体系。咱们接下来回简单地回顾几个流行的架构并作几个小的练习。关于某个例子若是你想了解的更详细一些,能够查看下方的连接。git

掌握设计模式会让人沉迷其中,全部必定要小心:相比阅读本文章以前,你可能会问更多像这样的问题: 由谁进行网络请求:Model?仍是ViewController?github

如何向新视图(View)的ViewModel中传递Modelweb

由谁建立一个新的VIPER模块:路由(Router)?仍是展现器(Presenter)? objective-c

...


为何在意架构的选择?

由于若是你不这样作,终有一天,你在调试一个拥有着数十个不一样方法和变量(things)的庞大的类文件时,你会发现你没法找到并修复此文件中的任何问题。天然地,也很难把这个类文件当作一个总体而熟稔于心,这样你可能老是会错过一些重要的细节。若是你的应用已经处于这样的境况,颇有多是这样:编程

  • 这个类是UIViewController的子类。
  • 你的数据直接存储在UIViewController中。
  • 你的UIViews什么都不作。
  • Model是哑数据结构(dumb data structure )。

dumb data structure: 只用来存储数据的结构,没有任何方法。详见:https://stackoverflow.com/questions/32944751/what-is-dumb-data-and-dumb-data-object-meanswift

  • 单元测试没有0覆盖。 即便你是按照苹果的指导方针并实现苹果的MVC模式,也会出现上述问题,全部不要难过。苹果的MVC模式存在着一些些问题,这点咱们稍后再说。

让咱们定义一下一个好的架构应该有的特色设计模式

  1. 能把代码职责均衡的解耦到不一样的功能类里。(Balanced distribution of responsibilities among entities with strict roles.)
  2. 可测试性(Testability usually comes from the first feature.)。
  3. 易用、维护成本低(Ease of use and a low maintenance cost.)。

解耦( Why Distribution ?)

在咱们试弄清楚事物是如何运做的时候,解耦能够保证大脑的负载均衡。若是你认为开发的(项目)越多你的大脑越能适应理解复杂的问题,那么你就是对的。可是这个能力不是线性扩展的而且很快就能达到上限。因此,解决复杂性的最简单的方式就是在多个实体间按照“单一责任原则” 拆分职责。promise

可测试(Why Testability ?)

对于那些已经习惯了单元测试的人来讲这并非一个问题,由于再添加了新的特性和重构了一个复杂的类后一般会运行失败。这意味着单元测试能够帮助开发者发现一些在运行时才会出现的问题,而且这些问题常见于安装在用户的手机上的应用上,此外要修复这些问题也须要大概一周的时间。

易用(Why Ease of use ?)

这个问题并不须要回答,但,值得一提的是:最好的代码老是那些没有被写出来的代码。所以,代码越少,错误也就越少。这也说明,总想着写最少代码的开发者不是由于他们懒,而且你也不该该由于一个更聪明的解决方案而忽视维护成本。


##MV(X)概要 现今,当咱们说起架构设计模式的时候,咱们有不少的选择。好比:

  • MVC
  • MVP
  • MVVM
  • VIPER

上述的前三个架构采起的是,把应用中的实体(entities)放入下面三个类别中其中一个内。

  • Models——负责域数据(domain data)和操做数据的数据访问层(Data access layer),可认为"Person"和"PersonDataProvider"类。
  • Views——负责表现层(GUI),对于iOS环境来讲就是全部以"UI"开头的类。
  • Controller/Presenter/ViewModel——模型(Model)和视图(View)的粘合剂、中介,一般的负责经过响应用户在视图(View)上的操做通知模型(Model)更新、并经过模型的改变来更新视图(View)。

实体解耦能让咱们:

  • 更好的理解它们(这点我么已经知道)
  • 复用(大多用于视图(View)和模型(Model))
  • 独立测试

让咱们想看一下MV(X)架构,以后再回过头来看VIPER


MVC

MVC前世

在讨论苹果的MVC架构时,先来看一下传统的MVC是什么样的。

Traditional MVC
在这种状况中,视图( View)是无状态的。一旦模型( Model)改变视图( View)仅仅只是被控制器( Controller)渲染而已。想象一下点击一个连接导航到其余地方后网页彻底加载出来的状况。尽管,iOS应用可使用传统的MVC架构,但这并无多大意义,由于架构自己就存在问题——三个实体( entities)之间联系太过紧密,每个实体都知道(引用)另外两个实体。这就致使了实体的复用性急剧降低——在你的应用中这并非你所想要的。出于这个缘由,咱们就不写MVC范例了。

Traditional MVC doesn't seems to be applicable to modern iOS development. 传统MVC架构看上去并不适合用于如今的iOS开发中。

Apple's MVC

预期

Expectation MVC

控制器(Controller)是视图(View)与模型(Model)二者之间的中介,这使得视图(View)与模型(Model)都不知道对方的存在。控制器(Controller)是可复用的最少的,对咱们来讲这一般很好,由于咱们必须有一个地方去放置一些不适合放在模型(Model)中且比较棘手的逻辑。 理论上,看起来很是简单,可是你感受到有些地方不对,是否是这样?你甚至据说过人们把MVC称做Massive View Controller。此外,视图控制器"瘦身"(View Controller offloading)成了iOS开发者中的一个重要话题。若是苹果只是采用传统MVC架构或者只是稍加改进,为何会出现这种状况?

MVC此生(现实状况)

Realistic Cocoa MVC
Cocoa MVC鼓励你使用大型的视图控制器( Massive View Controllers),因为他们都参与到了视图( View)的生命周期中了以致于很难说他们是分离的。尽管你仍有能力分流一些业务逻辑和数据转换功能到模型( Model)中,可是当涉及到把工做分流到视图( View)中去时你就诶有更多的选择了,由于在大多数时候视图( View)的全部职责是把动做传递到控制器( Controller)中。视图控制器( View Controller)最终最为全部控件的委托和数据源,一般负责调度和取消网络请求...应有尽有

你见过多少次这样的代码:

var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell
userCell.configureWithUser(user)
复制代码

cell这个视图是由Model直接配置数据的,所以这违反了MVC指南,可是这种状况无时无刻不在发生着,并且一般人们并不认为这样有什么错的。若是你严格的遵照MVC架构,那么你应该在Controller中配置cell数据,不用把Model传递到View中去,这会增长控制器的大小(复杂度)。

Cocoa MVC is reasonably unabbreviated the Massive View Controller. Cocoa MVC 被称做大型视图控制器是合理的。

在未说起单元测试(Unit Testing)MVC的问题并非很明显(但愿,你的项目中有单元测试)。因为你的视图控制器(View Controller)与视图(View)是紧耦合的,所以很难对其进行测试,由于你不得不很是有创造性的模拟视图和他们的生命周期,使用这种方式编写视图控制器(View Controller)代码,你要尽量的把业务逻辑和视图布局代码分离开来。

让咱们来看一个简单地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:", forControlEvent: .TouchUpInside)
	}
	func didTapButton(button: UIButton) {
		let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
		self.greetingLabel.text = greeting
	}
	
	// 布局代码在这儿
	......	
}
// 组合MVC
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
view.person = model
复制代码

MVC assembling can be performed in the presenting view controller 组合MVC能够在展现视图控制器(presenting view controller)中来完成

不是很容易测试,是否是这样?咱们能够把生成greeting的代码放入到GreetingModel类里并单独的进行测试,可是,在没有直接调用与UIView有关的方法(如:viewDidLoad, didTapButton,这些方法可能会加载全部的视图,不利于单元测试。)的状况下,咱们没法测试GreetingViewController中的任何展现逻辑(尽管上面的代码中没有太多这样的逻辑)。

事实上,在模拟器(如:iPhone4s)上加载并测试视图并不能保证在其余设备(如:iPad)上也能正常工做,因此,我建议从Unit Test目标(Unit Test target)配置中移除主应用程序(Host Application)并在模拟器上没有应用运行的状况下运行测试。

The interactions between the View and the Controller aren't really testable with Unit Tests. 视图和控制器之间的交互很难进行单元测试

综上所述,Cocoa MVC 多是一个至关糟糕的选择。让咱们按照文章开头定义的特色来评估一下这种架构模式:

  • 解耦(Distribution)——视图(View)和模型(Model)确实解耦了,然而,视图(View)和控制器(Controller)倒是紧密耦合的。
  • 可测试(Testability)——因为紧耦合的关系,你只能测试视图(Model)。
  • 易用(Ease of use)——同其余模式相比代码最少。此外,你们都熟悉它,所以,很用以掌握甚至是新手。

若是你没有打算在架构时耗费太多时间而且以为高成本的维护费用对你的小项目来讲是一种过分的浪费的话,那么Cocoa MVC就是你的最好选择。

Cocoa MVC is the best architectural pattern in term of the speed of the development. 在开发速度上面Cocoa MVC是最好的架构模式。


MVP

Cocoa MVC’s promises delivered

Passive View variant of MVP
是否是很像苹果的MVC架构?没错,确实如此,它就是 MVP(Passive View variant)。等下...是否是 Apple’s MVC事实上就是 MVP?并非,回想一下在 MVCViewController是紧密耦合的,然而,MVP的中介—— Presenter与View Controller的生命周期没有任何关系,而且很容易模拟View,因此 Presenter中没有任何布局代码,可是它却负责用数据和状态更新 View

What if i told you,the UIViewController is the View.

我告诉你,视图控制器就是视图。

MVP来讲,UIViewController的子类实际上就是视图(Views)不是展现器(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: GreetingViePresenter!
	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 go here
}

let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
let presenter = GreetingPresenter(view: view, person: model)
view.presenter = presenter
复制代码

关于组装的重要提示

MVP是第一个揭示出组装问题(assembly problem)的架构模式,而出现这个问题的缘由是它有三个实际上独立的层。因为咱们不想让视图(View)了解模型(Model),因此在展现视图控制器(也就是视图)执行组装是不正确的,所以咱们不得不在其余地方执行它。例如,咱们能够建立一个app范围(app-wide)的路由(Router)服务,让它来完成执行组装和视图到视图(View-to-View)的展现功能。这个问题不止在MVP中存在,在下面介绍的其余模式中也存在。

让咱们看一下MVP的特色:

  • 解耦(Distribution)——咱们在最大程度上分离了展现器(Presenter)和模型(Model),还有至关简单、轻薄的视图(dumb view)(在上述例子中的模型也很简单)。
  • 可测试性(Testability)——很棒,因为简单的视图,咱们能够测试大多数的业务逻辑。
  • 易用性(Easy of use)——在咱们简单不完整的例子中,相比于MVC这些代码成倍的增长了,可是与此同时,MVP模式的思路却更加的清晰。

MVP in iOS means superb testability and a lot of code iOS中的MVP架构意味着极好的可测试性和大量的代码。

####绑定和Hooters 还有一种类型的MVP架构模式——the Supervising Controller MVP。这个变种包括了视图和模型的直接绑定,展现器(The Supervising Controller)在处理动做的同时还能够改变视图。

Supervising Presenter variant of the MVP

可是,就如咱们已经知道的,模糊的职责拆分是不正确的,视图和模型的紧耦合也一样不可取。这和Cocoa桌面应用开发很类似。和传统的MVC同样,给有瑕疵的架构写例子没有任何意义。


MVVM

MV(X)类中近期最优秀的架构(The latest and the greatest of the MV(X) kind)

MVVM是MV(X)这类中最新的架构形式,因此,咱们但愿它可以解决MV(X)以前所面临的问题。

理论上,Model-View_ViewModel这种架构很棒。不只ViewModel,并且Mediator——至关于View Model,咱们都已经熟悉。

MVVM
它和 MVP很类似:

  • MVVM把视图控制器当作视图。
  • 视图(View)和模型(Model)之间不存在紧耦合。

另外,它还能够像MVP那样绑定;可是绑定不是发生在视图(View)和模型(Model)之间,而是视图(View)和视图模型(View Model)之间。

那么,iOS现实中的视图模型(View Model)的庐山面目是什么?它是你的视图及其状态的基本的UIKit的独立展现。视图模型触发模型的改变,并利用改变后的Model更新本身,因为咱们在视图和视图模型之间进行了绑定,视图也会根据视图模型的改变而改变。

绑定(Bindings)

绑定我在讲解MVP架构部分简单的提到过,这里咱们在对其进行一些讨论。绑定是从OSX开发而来的,并且iOS中并无这个概念。固然,咱们有KVO和通知(notifications),可是它的使用并无绑定方便。

因此,假若不想本身编写绑定代码,咱们还有两个选择:

事实上,现今,只要你听到“MVVM”你就会想到ReactiveCocoa,反之亦然。尽管使用简单地绑定也能够建立MVVM架构的项目,可是,ReactiveCocoa(或者同类的库)却可让你把使用MVVM架构的优点最大化。

关于Reactive库有一个残酷的现实须要面对:功能强大却伴随着巨大的职责。当使用Reactive库的时候极容易把不少事情搞混,若是出现错误,你可能须要花费不少的时间去在APP中定位问题所在,因此看一下下图的调用堆栈。

Reactive Debugging

在简单的例子中,使用FRF(functional reactive function:函数式响应式函数)库甚至KVO都显得大材小用,相反咱们显式使用*showGreeting方法让视图模型(View Model)更新,并使用greetingDidChange*回调函数这样一个简单地属性。

import UIKit
struct Person { //Model
	let firstName: String
	let lastName: String
}
protocol GreetingViewModelProtocol: class {
	var greeting: String? {get}
	// function to call when greeting did change
	var greetingDidChange: ((GreetingViewModelProtocol) -> ()) ? (get set)
	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
}

let model = Person(firstName: "David", lastName: "Blaine")
let viewModel = GreetingViewModel(person: model)
let view = GreetingViewController()
view.viewModel = viewModel
复制代码

再回过来看一下咱们的特色评估:

  • 解耦(Distribution)——在咱们上面的的简例中可能不太明显,事实上,MVVM的视图比MVP的视图拥有更多的职责。由于,前者经过绑定从视图模型(ViewModel)更新本身,然后者则是把全部的事件前置到Presenter中,也不对本身的状态进行更新。
  • 可测试性(Testability)——View ModelView一无所知,这可让咱们轻易地对其进行测试。也能够对视图(View)测试,但因为UIKit依赖,你可能想跳过她。
  • 易用性(Easy of use)——在咱们的例子中,MVVMMVP有一样的代码量,可是在实际的应用中,对于MVP你须要把全部事件从视图(View)前置到展现器(Presenter)并手动的更新视图,而对于MVVM,若是你使用了绑定则会变的很容易。

MVVM极其吸人眼球,它融合了上述全部架构的的优点,此外,因为它在视图(View)端进行了绑定,你能够不须要任何额外的代码对视图(View)进行更新。虽然如此,可测试性依然保持在一个很好的层次。


###VIPER ####把搭建乐高积木的经验拿到iOS应用设计中使用 VIPER使咱们最后的选择,这种架构尤其有趣,由于他不是属于MV(X)类的架构。

到目前为止,关于职责粒度的划分很是合理这点你确定赞同。VIPER在职责划分上面又作了一次迭代,此次咱们一共有五层。

VIPER

  • 交互器(Interactor)——包含与数据(Entities)或者网络相关的业务逻辑,向建立一个实体的对象或者从网络获取对象。为了这个目的,你须要用到一些ServicesManagers,这些不能算是VIPER的一部分,更确切的说只是些外部依赖。
  • 展现器(Presenter)——包含与UI相关(可是独立于UIKit)的业务逻辑,调用交互器(Interactor)中的方法。
  • 实体(Entities)——简单地数据对象,并非数据访问层,由于数据访问是交互器(Interactor)的职责。
  • 路由(Router)——用来链接VIPER中的模块。

大体上说,VIPER模块能够是一个界面,也能够是整个应用的用户界面(user story)——想象一下验证功能,它能够是一个界面也能够是几个相关联的界面。”乐高积木“块应该多大呢?——这取决你本身。

若是咱们同MV(X)这一类进行比较,咱们能够看到几个不一样的职责解耦之处:

  • 模型(Model(数据交互(data interaction))逻辑转移到了交互器Interactor)中,同时**实体(Entities)**做为单一的数据结构存在。
  • 只有控制器/展现器/视图模型的UI展现责任转移到了展现器(Presenter),而不是数据修改功能。
  • **VIPER是第一个明确的负责导航功能的架构模式,这点是经过路由(Router)**来解决的。

在iOS应用中,寻一个适当的方式进行页面路由是一个具备挑战性的工做,而MV(X)这类模式只是简单的避而不谈。

这个例子没有涉及到路由和模块间的交互,由于,这些话题在MV(X)这类模式中也没有说起。

import UIKIt
struct Person { // 实体(一般要比这个复杂,例如:NSManagedObject)
	let firstName: String
	let lastName: String
}
struct GreetingData { // 传递数据结构(不是实体)
	let greeting: String
	let subject: String
}

protocol GreetingOutput: class {
	func receiveGreetingData(greetingData: GreetingData)
}

class GreetingInteractor: GreetingProvider {
	weak var output: GreetingOutput!
	func provideGreetingData() {
		let person = Person(firstName: "David", lastName: "Blaine")// 一般来自于数据访问层
		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
}

let view = GreetingViewController()
let presenter = GreetingPresenter()
let interactor = GreetingInteractor()
view.eventHandler = presenter
presenter.greetingProvider = interactor
interactor.output = presenter
复制代码

再来看一下特色评估:

  • 解耦(Distribution)——毋庸置疑,VIPER架构在职责间解耦的表现最好。
  • 可测试性(Testability)——不足为奇,更好的解耦,更好的可测试性。
  • 易用性(Easy of use)——最后,上述两个的表现所花费的代价你已经猜出来了。你不得不写大量的没有多少职责的接口(interface)类。

乐高积木提如今哪里呢?

当使用VIPER时,感受就像用乐高积木搭建一座帝国大厦同样,这是一个有问题的信号。也许,对于你的应用来讲如今使用VIPER架构还为时过早,你能够考虑一个简单的架构。有些人则选择忽略这个问题,还继续大炮打麻雀——大材小用。我猜想他们以为将来他们的应用会所以而受益,尽管如今维护成本高的不合情理。若是你也这样想的话,我建议你试一下Generamba——一个能够生成VIPER架构的工具。尽管如此,对我我的来讲,这样就像在使用有自动瞄准系统的大炮同样而不是简单地投石机。


结论

咱们已经看过了几种架构模式,我但愿你们都能为各自的困惑找到答案,毫无疑问你会意识到这篇文章并无提供什么高招,因此,选择架构模式的关键是根据具体的状况进行权衡、取舍。

所以,在同一个应用中出现架构混合是很正常的一件事。例如:你一开始用的是MVC架构,忽然你意识到有一个特定的界面很难再用MVC架构进行有效的维护了,而后你就把它转换成了MVVM架构并且仅仅只是对这一个界面进行了转换。对于其余的界面若是MVC架构工做正常的话没有必要进行重构,由于这两个架构很容易兼容。

Make everything as simple as possible, but not simpler——Albert Einstein

尽量的简化一切,但并不简单——阿尔伯特·爱因斯坦

相关文章
相关标签/搜索