更新:在这里能够看到幻灯片 在iOS中使用MVC时感受怪怪的?对切换到MVVM有疑虑?据说过VIPER,但不知道是否值得?git
往下看,你将会找到这些问题的答案,若是还有疑问,请在评论区留言。github
你将了解到在iOS环境下如何进行系统架构设计。咱们将简单回顾一些流行的框架,并经过实践一些小例子来比较它们的理论。若是须要更多详细信息,请参考文章中出现的连接。编程
掌握设计模式可能会让人上瘾,因此要当心:你可能在阅读这篇文章以前已经问过本身一些问题,好比说: 谁应该拥有联网请求:Model仍是Controller? 如何将Model传递到新View的View Model中? 谁建立了一个新的VIPER模块:Router仍是Presenter?设计模式
假若有一天,你在调试一个实现了几十种功能的庞大的类时,你会发现本身很难找到并修复你的类中的任何错误。而且,很难把这个类做为一个总体来考虑,所以,你总会忽略一些重要的细节。若是你的应用程序中已经出现了这种状况,那么颇有可能:bash
即便你遵循了苹果的指导方针并实现了苹果的MVC模式,这种状况仍是会发生的,因此不要难过。苹果的MVC有点问题,这个咱们稍后再谈。服务器
让咱们定义一个优秀系统结构的特征: 1.角色间职责的清晰分配(分布式)。 2.可测试性一般来自第一个特性(没必要担忧:使用适当的系统结构是很容易的)。 3.使用方便,维护成本低。网络
当咱们想弄清楚某些事情是如何运做时,采用分布式能让咱们的大脑思路清晰。若是你认为你开发越多,你的大脑就越能理解复杂性,那么你是对的。但这种能力不是线性的,很快就会达到上限。所以,克服复杂性的最简单方法是按照单一职责原则在多个实体之间划分职责。数据结构
对于那些已经习惯了单元测试的人来讲,这一般不是问题,由于在添加了新的特性或者要增长一些类的复杂性以后一般会失败。这意味着测试可以下降应用程序在用户的设备上发生问题的几率,那时修复也许须要一个星期(审核)才能到达用户。架构
这并不须要回答,但值得一提的是,最好的代码是从未编写过的代码。所以,你拥有的代码越少,你拥有的bug就越少。这意味着编写更少代码的愿望决不能仅仅由开发人员的懒惰来解释,你不该该偏心看起来更聪明的解决方案而忽视它的维护成本。
如今咱们在架构设计模式上有不少选择:
他们中的三个假设将应用程序的实体分红3类:
这种划分能让咱们:
让咱们从MV(X)开始,稍后在回到VIPER:
在讨论苹果对MVC的见解以前,让咱们先看看传统的MVC。
在上图的状况下,View是无状态的。一旦Model被改变,Controller就会简单地渲染它。例如:网页彻底加载后,一旦你按下连接,就导航到其余地方。
虽然在iOS应用用传统的MVC架构也能够实现,但这并无多大意义,因为架构问题 ——三个实体是紧耦合的,每一个实体和其余两个通讯。这大大下降了可重用性——这可不是你但愿在你的应用程序看到的。出于这个缘由,咱们甚至不想编写规范的MVC示例。
传统的MVC彷佛不适用于现代IOS开发。
Controller是View和Model之间的中介,这样他们就解耦了。最小的可重用单元是Controller,这对咱们来讲是个好消息,由于咱们必须有一个来放那些不适合放入Model的复杂业务逻辑的地方。
从理论上讲,它看起来很简单,但你以为有些地方不对,对吧?你甚至听到有人说MVC全称应该改成Massive View Controller(大量的视图控制器)。此外,为View controller减负也成为iOS开发者面临的一个重要话题。
若是苹果只接受传统的MVC并改进了它,为何会出现这种状况呢?
Cocoa MVC鼓励人们编写大规模的视图控制器,并且因为它们涉及View的生命周期,因此很难说它们(View和Controller)是分离的。
虽然你仍有能力将一些业务逻辑和数据转换成Model,但你没办法将View从Controller中分离。在大多数时候全部View的责任是把事件传递给Controller。
ViewController最终演变成一个其余人的delegate和data source,一般负责分派和取消网络请求…你明白的。
你见过多少这样的代码?:
var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell
userCell.configureWithUser(user)
复制代码
Cell(一个View)跟一个Model直接绑定了!因此MVC准则被违反了,可是这种状况老是发生,一般人们不会以为它是错误的。若是你严格遵循MVC,那么你应该从Controller配置cell,而不是将Model传递到cell中,这将增大Controller。
Cocoa MVC 的全称应该是 Massive View Controller.
在单元测试以前,这个问题可能并不明显(但愿在你的项目中是这样)。
因为视图控制器与视图紧密耦合,所以很难测试——由于在编写视图控制器的代码时,你必须模拟View的生命周期,从而使你的业务逻辑尽量地与View层的代码分隔开来。
让咱们看一看简单的操场例子:
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
}
// 这里写布局代码
}
// 组装MVC
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
view.person = model;
复制代码
MVC在可见的ViewController中进行组装
这彷佛不太容易测试,对吗?
咱们能够将greeting移动到新的GreetingModel类中并分别进行测试,但咱们不能在不调用GreetingViewController的有关方法(viewDidLoad, didTapButton,这将会加载全部的View) 的状况下测试UIView中的显示逻辑(虽然在上面的例子中没有太多这样的逻辑)。这不利于单元测试。
事实上,在一个模拟器(如iPhone 4S)中测试UIViews并不能保证它会在其余设备良好的工做(例如iPad),因此我建议从你的单元测试Target中删除“Host Application”选项,而后脱离应用程序运行你的测试。
View和Controller之间的交互在单元测试中是不可测试的。
如此看来,Cocoa MVC 模式 彷佛是一个很糟糕的选择。可是让咱们根据文章开头定义的特性来评估它:
若是你不肯意在项目的架构上投入太多的时间,那么Cocoa MVC 就是你应该选择的模式。并且你会发现用其余维护成本较高的模式开发小的应用是一个致命的错误。
Cocoa MVC是开发速度最快的架构模式。
这看起来不正是苹果的MVC吗?是的,它的名字是MVP(Passive View variant,被动视图变体)。等等...这是否是意味着苹果的MVC其实是MVP?不,不是这样。若是你仔细回忆一下,View是和Controller紧密耦合的,可是MVP的中介Presenter并无对ViewController的生命周期作任何改变,所以View能够很容易的被模拟出来。在Presenter中根本没有和布局有关的代码,可是它却负责更新View的数据和状态。
假如告诉你,UIViewController就是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: 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
}
// 布局代码
}
// 装配 MVP
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
let presenter = GreetingPresenter(view: view, person: model)
view.presenter = presenter
复制代码
MVP是第一个揭示装配问题的模式,由于它有三个独立的层。既然咱们不但愿View和Model耦合,那么在显示的View Controller(其实就是View)中处理这种协调的逻辑就是不正确的,所以咱们须要在其余地方来作这些事情。例如,咱们能够作基于整个App范围内的路由服务,由它来负责执行协调任务,以及View到View的展现。这不只仅是在MVP模式中必须处理的问题,同时也存在于如下集中方案中。
让咱们看看MVP的特色:
iOS 中的MVP意味着可测试性强、代码量大。
还有一些其余形态的MVP —— Supervising Controller MVP(监听Controller的MVP)。这个变体的变化包括View和Model之间的直接绑定,可是Presenter(Supervising Controller)仍然来管理来自View的动做事件,同时也能胜任对View的更新。
可是咱们以前就了解到,模糊的职责划分是很是糟糕的,更况且将View和Model紧密的联系起来。这和Cocoa的桌面开发的原理有些类似。
和传统的MVC同样,写这样的例子没有什么价值,故再也不给出。
MVVM架构是MV(X)系列最新的成员,咱们但愿它已经考虑到MV(X)系列中以前已经出现的问题。
从理论层面来说Model-View-ViewModel看起来不错,咱们已经很是熟悉View和Model,以及Meditor(中介),在这里它叫作View Model。
它和MVP模式看起来很像:
此外,它还有像Supervising版本的MVP那样的绑定功能,但这个绑定不是在View和Model之间而是在View和ViewModel之间。
那么在iOS中ViewModel到底表明了什么?它基本上就是UIKit下的独立控件以及控件的状态。ViewModel调用会改变Model同时会将Model的改变动新到自身而且由于咱们绑定了View和ViewModel,第一步就是相应的更新状态。
我在MVP部分已经提到这点了,可是在这里咱们来继续讨论。
绑定是从OS X开发中衍生出来的,可是咱们没有在iOS开发中使用它们。固然咱们有KVO通知,但它们没有绑定方便。
若是咱们本身不想本身实现,那么咱们有两种选择:
事实上,尤为是最近,你听到MVVM就会想到ReactiveCoca,反之亦然。尽管经过简单的绑定来使用MVVM是可实现的,可是ReactiveCocoa(或其变体)却能更好的发挥MVVM的特色。
函数响应式框架有一个残酷的事实:强大的能力来自于巨大的责任。当你开始使用Reactive的时候有很大的可能就会把事情搞砸。换句话来讲就是,若是发现了一些错误,调试出这个bug可能会花费大量的时间,看下函数调用栈:
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
}
// 装配 MVVM
let model = Person(firstName: "David", lastName: "Blaine")
let viewModel = GreetingViewModel(person: model)
let view = GreetingViewController()
view.viewModel = viewModel
复制代码
让咱们再来看看关于三个特性的评估:
MVVM是很是有吸引力的,由于它集合了上述方法的优势,而且因为在View层的绑定,它并不须要其余附加的代码来更新View,尽管这样,可测试性依然很强。
VIPER是咱们最后要介绍的,因为不是来自于MV(X)系列,它具有必定的趣味性。
到目前为止,你必须赞成划分责任的粒度是很好的选择。VIPER在责任划分层面进行了迭代,VIPER分为五个层次:
基本上,VIPER的模块能够是一个屏幕或者用户使用应用的整个过程 —— 例如认证过程,能够由一屏完成或者须要几步才能完成。你想让模块多大,这取决于你。
当咱们把VIPER和MV(X)系列做比较时,咱们会在职责划分方面发现一些不一样:
找到一个适合的方法来实现路由对于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
}
// 布局代码
}
// 装配 VIPER 模块(不包含路由)
let view = GreetingViewController()
let presenter = GreetingPresenter()
let interactor = GreetingInteractor()
view.eventHandler = presenter
presenter.view = view
presenter.greetingProvider = interactor
interactor.output = presenter
复制代码
让咱们再来评估一下特性:
当使用VIPER时,你可能想像用乐高积木来搭建一个城堡,这个想法可能存在一些问题。
也许,如今就应用VIPER架构还为时过早,考虑一些更为简单的模式反而会更好。一些人会忽略这些问题,大材小用。假定他们笃信VIPER架构会在将来给他们的应用带来一些好处,虽然如今维护起来确实是有些费劲。若是你也持这样的观点,我为你推荐 Generamba 这个用来搭建VIPER架构的工具。虽然我我的感受这是在用高射炮打蚊子。
咱们研究了几种架构模式,但愿你能找到一些困扰你的问题的答案。但毫无疑问经过阅读这篇文章你应该已经认识到了没有绝对的解决方案。因此架构模式的选择须要根据实际状况进行利弊分析。
所以,在同一应用程序中混合架构是很天然的。例如:你开始的时候使用MVC,而后忽然意识到一个页面在MVC模式下的变得愈来愈难以维护,而后就切换到MVVM架构,可是仅仅针对这一个页面。并无必要对哪些MVC模式下运转良好的页面进行重构,由于两者是能够并存的。
让一切尽量简单,但不是愚蠢。 —— 阿尔伯特·爱因斯坦