原文地址: http://www.cocoachina.com/ios/20160108/14916.html?utm_source=tuicool&utm_medium=referralhtml
在 iOS 中使用 MVC 架构感受很奇怪? 迁移到MVVM架构又怀有疑虑?据说过 VIPER 又不肯定是否真的值得切换?ios
相信你会找到以上问题的答案,若是没找到请在评论中指出。git
你将要整理出你在 iOS 环境下全部关于架构模式的知识。咱们将带领你们简要的回顾一些流行的架构,而且在理论和实践上对它们进行比较,经过一些小的例子深化你的认知。若是对文中提到的一些关键词有兴趣,能够点击链接去查看更详细的内容。github
掌控设计模式可能会令人上瘾,因此要小心,你可能会对一些问题清晰明了,再也不像阅读以前那样迷惑,好比下面这些问题:objective-c
谁应该来负责网络请求?Model 仍是 Controller ?编程
应该怎样向一个新的页面的 ViewModel 传入一个 Model ?swift
谁来建立一个 VIPER 模块,是 Router 仍是 Presenter ?设计模式
为何要关注架构设计?服务器
由于假如你不关心架构,那么总有一天,须要在同一个庞大的类中调试若干复杂的事情,你会发如今这样的条件下,根本不可能在这个类中快速的找到以及有效的修改任何bug.固然,把这样的一个类想象为一个总体是困难的,所以,有可能一些重要的细节总会在这个过程当中会被忽略。若是如今的你正是处于这样一个开发环境中,颇有可能具体的状况就像下面这样:网络
这个类是一个UIViewController的子类
数据直接在UIViewController中存储
UIView类几乎不作任何事情
Model 仅仅是一个数据结构
单元测试覆盖不了任何用例
以上这些状况仍旧会出现,即便是你遵循了Apple的指导原则而且实现了其 MVC(模式,因此,大可没必要惊慌。Apple所提出的 MVC 模式存在一些问题,咱们以后会详述。
在此,咱们能够定义一个好的架构应该具有的特色:
任务均衡分摊给具备清晰角色的实体
可测试性一般都来自与上一条(对于一个合适的架构是很是容易)
易用性和低成本维护
为何采用分布式?
采用分布式能够在咱们要弄清楚一些事情的原理时保持一个均衡的负载。若是你认为你的开发工做越多,你的大脑越能习惯复杂的思惟,其实这是对的。可是,不能忽略的一个事实是,这种思惟能力并非线性增加的,并且也并不能很快的到达峰值。因此,可以打败这种复杂性的最简单的方法就是在遵循 单一功能原则 的前提下,将功能划分给不一样的实体。
为何须要易测性?
其实这条要求对于哪些习惯了单元测试的人并非一个问题,由于在添加了新的特性或者要增长一些类的复杂性以后一般会失效。这就意味着,测试能够避免开发者在运行时才发现问题----当应用到达用户的设备,每一次维护都须要浪费长达至少[一周](http://appreviewtimes.com)的时间才能再次分发给用户。
为何须要易用性?
这个问题没有固定的答案,但值得一提的是,最好的代码是那些从未写过的代码。所以,代码写的越少,Bug就越少。这意味着但愿写更少的代码不该该被单纯的解释为开发者的懒惰,并且也不该该由于偏心更聪明的解决方案而忽视了它的维护开销。
MV(X)系列概要
当今咱们已经有很架构设计模式方面的选择:
前三种设计模式都把一个应用中的实体分为如下三类:
Models--负责主要的数据或者操做数据的数据访问层,能够想象 Perspn 和 PersonDataProvider 类。
Views--负责展现层(GUI),对于iOS环境能够联想一下以 UI 开头的全部类。
Controller/Presenter/ViewModel--负责协调 Model 和 View,一般根据用户在View上的动做在Model上做出对应的更改,同时将更改的信息返回到View上。
将实体进行划分给咱们带来了如下好处:
更好的理解它们之间的关系
复用(尤为是对于View和Model)
独立的测试
让咱们开始了解MV(X)系列,以后再返回到VIPER模式。
MVC的过去
在咱们探讨Apple的MVC模式以前,咱们来看下传统的MVC模式。
传统的MVC
在这里,View并无任何界限,仅仅是简单的在Controller中呈现出Model的变化。想象一下,就像网页同样,在点击了跳转到某个其余页面的链接以后就会彻底的从新加载页面。尽管在iOS平台上实现这这种MVC模式是没有任何难度的,可是它并不会为咱们解决架构问题带来任何裨益。由于它自己也是,三个实体间相互都有通讯,并且是紧密耦合的。这很显然会大大下降了三者的复用性,而这正是咱们不肯意看到的。鉴于此咱们再也不给出例子。
“传统的MVC架构不适用于当下的iOS开发”
苹果推荐的MVC--愿景
Cocoa MVC
因为Controller是一个介于View 和 Model之间的协调器,因此View和Model之间没有任何直接的联系。Controller是一个最小可重用单元,这对咱们来讲是一个好消息,由于咱们总要找一个地方来写逻辑复杂度较高的代码,而这些代码又不适合放在Model中。
理论上来说,这种模式看起来很是直观,但你有没有感到哪里有一丝诡异?你甚至据说过,有人将MVC的缩写展开成(Massive View Controller),更有甚者,为View controller减负也成为iOS开发者面临的一个重要话题。若是苹果继承而且对MVC模式有一些进展,全部这些为何还会发生?
苹果推荐的MVC--事实
Realistic Cocoa MVC
Cocoa的MVC模式驱令人们写出臃肿的视图控制器,由于它们常常被混杂到View的生命周期中,所以很难说View和ViewController是分离的。尽管仍能够将业务逻辑和数据转换到Model,可是大多数状况下当须要为View减负的时候咱们却无能为力了,View的最大的任务就是向Controller传递用户动做事件。ViewController最终会承担一切代理和数据源的职责,还负责一些分发和取消网络请求以及一些其余的任务,所以它的名字的由来...你懂的。
你可能会看见过不少次这样的代码:
var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell userCell.configureWithUser(user)
这个cell,正是由View直接来调用Model,因此事实上MVC的原则已经违背了,可是这种状况是一直发生的甚至于人们不以为这里有哪些不对。若是严格遵照MVC的话,你会把对cell的设置放在 Controller 中,不向View传递一个Model对象,这样就会大大增长Controller的体积。
“Cocoa 的MVC被写成Massive View Controller 是不无道理的。”
直到进行单元测试的时候才会发现问题愈来愈明显。由于你的ViewController和View是紧密耦合的,对它们进行测试就显得很艰难--你得有足够的创造性来模拟View和它们的生命周期,在以这样的方式来写View Controller的同时,业务逻辑的代码也逐渐被分散到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 } // layout code goes here } // Assembling of MVC let model = Person(firstName: "David", lastName: "Blaine") let view = GreetingViewController() view.person = model;
“MVC能够在一个正在显示的ViewController中实现”
这段代码看起来可测试性并不强,咱们能够把和greeting相关的都放到GreetingModel中而后分开测试,可是这样咱们就没法经过直接调用在GreetingViewController中的UIView的方法(viewDidLoad和didTapButton方法)来测试页面的展现逻辑了,由于一旦调用则会使整个页面都变化,这对单元测试来说并非什么好消息。
事实上,在单独一个模拟器中(好比iPhone 4S)加载并测试UIView并不能保证在其余设备中也能正常工做,所以我建议在单元测试的Target的设置下移除"Host Application"项,而且不要在模拟器中测试你的应用。
“View和Controller的接口并不适合单元测试。”
以上所述,彷佛Cocoa MVC 看起来是一个至关差的架构方案。咱们来从新评估一下文章开头咱们提出的MVC一系列的特征:
任务均摊--View和Model确实是分开的,可是View和Controller倒是紧密耦合的
可测试性--因为糟糕的分散性,只能对Model进行测试
易用性--与其余几种模式相比最小的代码量。熟悉的人不少,于是即便对于经验不那么丰富的开发者来说维护起来也较为容易。
若是你不想在架构选择上投入更多精力,那么Cocoa MVC无疑是最好的方案,并且你会发现一些其余维护成本较高的模式对于你所开发的小的应用是一个致命的打击。
“就开发速度而言,Cocoa MVC是最好的架构选择方案。”
MVP 实现了Cocoa的MVC的愿景
Passive View variant of MVP
这看起来不正是苹果所提出的MVC方案吗?确实是的,这种模式的名字叫作MVC,可是,这就是说苹果的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 } // 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是第一个如何协调整合三个实际上分离的层次的架构模式,既然咱们不但愿View涉及到Model,那么在显示的View Controller(其实就是View)中处理这种协调的逻辑就是不正确的,所以咱们须要在其余地方来作这些事情。例如,咱们能够作基于整个App范围内的路由服务,由它来负责执行协调任务,以及View到View的展现。这个出现而且必须处理的问题不只仅是在MVP模式中,同时也存在于如下集中方案中。
咱们来看下MVP模式下的三个特性的分析:
任务均摊--咱们将最主要的任务划分到Presenter和Model,而View的功能较少(虽然上述例子中Model的任务也并很少)。
可测试性--很是好,因为一个功能简单的View层,因此测试大多数业务逻辑也变得简单
易用性--在咱们上边不切实际的简单的例子中,代码量是MVC模式的2倍,但同时MVP的概念却很是清晰
“iOS 中的MVP意味着可测试性强、代码量大。”
MVP--绑定和信号
还有一些其余形态的MVP--监控控制器的MVP。
这个变体包含了View和Model之间的直接绑定,可是Presenter仍然来管理来自View的动做事件,同时也能胜任对View的更新。
Supervising Presenter variant of the MVP
可是咱们以前就了解到,模糊的职责划分是很是糟糕的,更况且将View和Model紧密的联系起来。这和Cocoa的桌面开发的原理有些类似。
和传统的MVC同样,写这样的例子没有什么价值,故再也不给出。
MVVM--最新且是最伟大的MV(X)系列的一员
MVVM架构是MV(X)系列最新的一员,所以让咱们但愿它已经考虑到MV(X)系列中以前已经出现的问题。
从理论层面来说MVVM看起来不错,咱们已经很是熟悉View和Model,以及Meditor,在MVVM中它是View Model。
MVVM
它和MVP模式看起来很是像:
MVVM将ViewController视做View
在View和Model之间没有紧密的联系
此外,它还有像监管版本的MVP那样的绑定功能,但这个绑定不是在View和Model之间而是在View和ViewModel之间。
那么问题来了,在iOS中ViewModel实际上表明什么?它基本上就是UIKit下的每一个控件以及控件的状态。ViewModel调用会改变Model同时会将Model的改变动新到自身而且由于咱们绑定了View和ViewModel,第一步就是相应的更新状态。
绑定
我在MVP部分已经提到这点了,可是该部分咱们仍会继续讨论。
若是咱们本身不想本身实现,那么咱们有两种选择:
基于KVO的绑定库如 RZDataBinding 和 SwiftBond
彻底的函数响应式编程,好比像ReactiveCocoa、RxSwift或者 PromiseKit
事实上,尤为是最近,你听到MVVM就会想到ReactiveCoca,反之亦然。尽管经过简单的绑定来使用MVVM是可实现的,可是ReactiveCocoa却能更好的发挥MVVM的特色。
可是关于这个框架有一个不得不说的事实:强大的能力来自于巨大的责任。当你开始使用Reactive的时候有很大的可能就会把事情搞砸。换句话来讲就是,若是发现了一些错误,调试出这个bug可能会花费大量的时间,看下函数调用栈:
Reactive Debugging
在咱们简单的例子中,FRF框架和KVO被过渡禁用,取而代之地咱们直接去调用showGreeting方法更新ViewModel,以及经过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的View要比MVP中的View承担的责任多。由于前者经过ViewModel的设置绑定来更新状态,然后者只监听Presenter的事件但并不会对本身有什么更新。
可测试性 -- ViewModel不知道关于View的任何事情,这容许咱们能够轻易的测试ViewModel。同时View也能够被测试,可是因为属于UIKit的范畴,对他们的测试一般会被忽略。
易用性 -- 在咱们例子中的代码量和MVP的差很少,可是在实际开发中,咱们必须把View中的事件指向Presenter而且手动的来更新View,若是使用绑定的话,MVVM代码量将会小的多。
“MVVM很诱人,由于它集合了上述方法的优势,而且因为在View层的绑定,它并不须要其余附加的代码来更新View,尽管这样,可测试性依然很强。”
VIPER--把LEGO建筑经验迁移到iOS app的设计
VIPER是咱们最后要介绍的,因为不是来自于MV(X)系列,它具有必定的趣味性。
迄今为止,划分责任的粒度是很好的选择。VIPER在责任划分层面进行了迭代,VIPER分为五个层次:
VIPER
交互器 -- 包括关于数据和网络请求的业务逻辑,例如建立一个实体(数据),或者从服务器中获取一些数据。为了实现这些功能,须要使用服务、管理器,可是他们并不被认为是VIPER架构内的模块,而是外部依赖。
展现器 -- 包含UI层面的业务逻辑以及在交互器层面的方法调用。
实体 -- 普通的数据对象,不属于数据访问层次,由于数据访问属于交互器的职责。
路由器 -- 用来链接VIPER的各个模块。
基本上,VIPER模块能够是一个屏幕或者用户使用应用的整个过程--想一想认证过程,能够由一屏完成或者须要几步才能完成,你的模块指望是多大的,这取决于你。
当咱们把VIPER和MV(X)系列做比较时,咱们会在任务均摊性方面发现一些不一样:
Model 逻辑经过把实体做为最小的数据结构转换到交互器中。
Controller/Presenter/ViewModel的UI展现方面的职责移到了Presenter中,可是并无数据转换相关的操做。
VIPER是第一个经过路由器实现明确的地址导航模式。
“找到一个适合的方法来实现路由对于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是任务划分中的佼佼者。
可测试性 -- 不出意外地,更好的分布性就有更好的可测试性。
易用性 -- 最后你可能已经猜到了维护成本方面的问题。你必须为很小功能的类写出大量的接口。
什么是LEGO
当使用VIPER时,你的感受就像是用乐高积木来搭建一个城堡,这也是一个代表当前存在一些问题的信号。可能如今就应用VIPER架构还为时过早,考虑一些更为简单的模式可能会更好。一些人会忽略这些问题,大材小用。假定他们笃信VIPER架构会在将来给他们的应用带来一些好处,虽然如今维护起来确实是有些不合理。若是你也持这样的观点,我为你推荐 Generamba 这个用来搭建VIPER架构的工具。虽然我我的感受,使用起来就像加农炮的自动瞄准系统,而不是简单的像投石器那样的简单的抛掷。
总结
咱们了解了集中架构模式,但愿你已经找到了究竟是什么在困扰你。毫无疑问经过阅读本篇文章,你已经了解到其实并无彻底的银弹。因此选择架构是一个根据实际状况具体分析利弊的过程。
所以,在同一个应用中包含着多种架构。好比,你开始的时候使用MVC,而后忽然意识到一个页面在MVC模式下的变得愈来愈难以维护,而后就切换到MVVM架构,可是仅仅针对这一个页面。并无必要对哪些MVC模式下运转良好的页面进行重构,由于两者是能够并存的。