在这里可以看到幻灯片
你可能在阅读这篇文章之前已经问过自己一些问题,比如说:
谁应该拥有联网请求:Model还是Controller, 或者viewModel, presenter?
谁该处理数据(包括服务器数据,本地数据),谁该处理展示逻辑?
针对这些问题的不同回答就产生了不同的架构.
为什么要纠结选择什么架构呢?
假如有一天,你在调试一个实现了几十种功能的庞大的类时,你会发现自己很难找到并修复你的类中的任何错误。并且,很难把这个类作为一个整体来考虑,因此,你总会忽略一些重要的细节。如果你的应用程序中已经出现了这种情况,那么很有可能:
- 这类是UIViewController类。
- UIViewController直接存储和处理你的数据
- 你的UIView中几乎没有做任何事情
- Model仅仅是一个数据结构
- 单元测试覆盖不了任何内容
即使你遵循了苹果的指导方针并实现了苹果的MVC模式,这种情况还是会发生的,所以不要难过。苹果的MVC有点问题,这个我们稍后再谈。
让我们定义一个优秀系统结构的特征:
1.角色间职责的清晰分配(分布式)。
2.可测试性通常来自第一个特性(不必担心:使用适当的系统结构是很容易的)。
3.使用方便,维护成本低。
为什么要采用分布式
当我们想弄清楚某些事情是如何运作时,采用分布式能让我们的大脑思路清晰。如果你认为你开发越多,你的大脑就越能理解复杂性,那么你是对的。但这种能力不是线性的,很快就会达到上限。因此,克服复杂性的最简单方法是按照单一职责原则在多个实体之间划分职责。
为什么要可测试
对于那些已经习惯了单元测试的人来说,这通常不是问题,因为在添加了新的特性或者要增加一些类的复杂性之后通常会失败。这意味着测试能够降低应用程序在用户的设备上发生问题的概率,那时修复也许需要一个星期(审核)才能到达用户。
为什么要易用性
这并不需要回答,但值得一提的是,最好的代码是从未编写过的代码。因此,你拥有的代码越少,你拥有的bug就越少。这意味着编写更少代码的愿望决不能仅仅由开发人员的懒惰来解释,你不应该偏爱看起来更聪明的解决方案而忽视它的维护成本。
MV(X) 简介
现在我们在架构设计模式上有很多选择:
他们中的三个假设将应用程序的实体大体分成3类:
- Models — 负责保存数据或数据访问层,操纵数据,例如“人”或“提供数据的人”。
- Views — 负责表示层(GUI),iOS环境下通常以“UI”前缀。
- Controller/Presenter/ViewModel — Model和View之间的中介,一般负责在用户操作View时更新Model,以及当Model变化时更新View。
这种划分能让我们:
- 更好地理解它们(如我们所知)
- 重用它们(尤其是View和Model)
- 独立地进行测试(单元测试)
让我们从MV(X)开始,稍后在回到VIPER:
MVC
曾经
在讨论苹果对MVC的看法之前,让我们先看看传统的MVC。
传统的MVC
在上图的情况下,View是无状态的。一旦Model被改变,Controller就会简单地渲染它。
虽然在iOS应用用传统的MVC架构也可以实现,但这并没有多大意义,由于架构问题 ——三个实体是紧耦合的,每个实体和其他两个通信。这大大降低了可重用性——这可不是你希望在你的应用程序看到的。出于这个原因,我们甚至不想编写规范的MVC示例。
传统的MVC似乎不适用于现代IOS开发。
苹果的MVC
愿景:
Cocoa MVC
Controller是View和Model之间的中介,这样他们就解耦了。最小的可重用单元是Controller,这对我们来说是个好消息,因为我们必须有一个来放那些不适合放入Model的复杂业务逻辑的地方。
从理论上讲,它看起来很简单,但你觉得有些地方不对,对吧?你甚至听到有人说MVC全称应该改为Massive View Controller(大量的视图控制器)。此外,为View controller减负也成为iOS开发者面临的一个重要话题。
如果苹果只接受传统的MVC并改进了它,为什么会出现这种情况呢?
实际情况:
事实上的Cocoa MVC
Cocoa MVC鼓励人们编写大规模的视图控制器,而且由于它们涉及View的生命周期,所以很难说它们(View和Controller)是分离的。
虽然你仍有能力将一些业务逻辑和数据转换成Model,但你没办法将View从Controller中分离。在大多数时候所有View的责任是把事件传递给Controller。
ViewController最终演变成一个其他人的delegate和data source,通常负责分派和取消网络请求, 处理数据, 把服务器数据转换成UI需要的数据…你明白的。
你见过多少这样的代码?:
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层的代码分隔开来。
让我们看一看简单的例子, 在一个vc上有一个按钮, 点击后再label上显示一句欢迎语:
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中的显示逻辑(虽然在上面的例子中没有太多这样的逻辑)。这不利于单元测试。
View和Controller之间的交互在单元测试中是不可测试的。
如此看来,Cocoa MVC 模式 似乎是一个很糟糕的选择。但是让我们根据文章开头定义的特性来评估它:
- 职责拆分 — View和Model实现了分离,但是View与Controller仍是紧耦合。
- 可测性 — 由于模式的原因,你只能测试你的Model。
- 易用性 — 相比于其他模式代码量最少。此外,每个人都熟悉它,即使经验不太丰富的开发人员也能够维护它。
如果你不愿意在项目的架构上投入太多的时间,那么Cocoa MVC 就是你应该选择的模式。而且你会发现用其他维护成本较高的模式开发小的应用是一个致命的错误。
Cocoa MVC是开发速度最快的架构模式。
MVP
MVP 实现了Cocoa的MVC的愿景
Passive View 变体 — MVP
这看起来不正是苹果的MVC吗?是的,但是它的名字是MVP(Passive View variant,被动视图变体)。等等...这是不是意味着苹果的MVC实际上是MVP?不,不是这样。如果你仔细回忆一下,View是和Controller紧密耦合的,但是MVP的中介Presenter并没有对ViewController的生命周期做任何改变,因此View可以很容易的被模拟出来。在Presenter中根本没有和布局有关的代码,但是它却负责更新View的数据和状态。
在MVP中,V代表的含义不只是UIView, 也可以代表UIViewController。这种模式的可测试性得到了极大的提高,付出的代价是开发速度的一些降低,因为必须要做一些手动的数据和事件绑定,从下例中可以看出:
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
装配问题的重要说明
view层只做了2类事情: 1.把事件传递出去; 2.提供方法更新UI
MVP是第一个揭示装配问题的模式,因为它有三个独立的层。既然我们不希望View和Model耦合,那么在显示的View Controller(其实就是View)中处理这种协调的逻辑就是不正确的,因此我们需要在其他地方来做这些事情。例如,我们可以做基于整个App范围内的路由服务,由它来负责执行协调任务,以及View到View的展示。这不仅仅是在MVP模式中必须处理的问题,同时也存在于以下集中方案中。
让我们看看MVP的特点:
- 职责拆分 — 我们将最主要的任务划分到Presenter和Model,而View的功能较少(虽然上述例子中Model的任务也并不多)。
- 可测性 — 非常好,基于一个功能简单的View层,可以测试大多数业务逻辑
- 易用性 — 在我们上边不切实际的简单的例子中,代码量是MVC模式的2倍,但同时MVP的概念却非常清晰。
iOS 中的MVP意味着可测试性强、代码量大。
MVVM
最新且是最伟大的MV(X)系列的一员
MVVM架构是MV(X)系列最新的成员,我们希望它已经考虑到MV(X)系列中之前已经出现的问题。
从理论层面来讲Model-View-ViewModel看起来不错,我们已经非常熟悉View和Model,以及Meditor(中介),在这里它叫做View Model。
它和MVP模式看起来很像:
- MVVM也将ViewController视作View
- 在View和Model之间没有耦合
那么在iOS中ViewModel到底代表了什么?它基本上就是UIKit下的独立控件以及控件的状态。ViewModel调用会改变Model同时会将Model的改变更新到自身并且因为我们绑定了View和ViewModel,View和model之间的耦合也不存在了。
绑定
我在MVP部分已经提到这点了,但是在这里我们来继续讨论。
绑定是从OS X开发中衍生出来的,但是我们没有在iOS开发中使用它们。当然我们有KVO通知,但它们没有绑定方便。
如果我们自己不想自己实现,那么我们有两种选择:
- 基于KVO进行绑定,如 RZDataBinding 和 SwiftBond
- 完全的函数响应式编程,例如ReactiveCocoa、RxSwift或者 PromiseKit
事实上,尤其是最近,你听到MVVM就会想到ReactiveCoca,反之亦然。尽管通过简单的绑定来使用MVVM是可实现的,但是ReactiveCocoa(或其变体)却能更好的发挥MVVM的特点。
函数响应式框架有一个残酷的事实:强大的能力来自于巨大的责任。当你开始使用Reactive的时候有很大的可能就会把事情搞砸。换句话来说就是,如果发现了一些错误,调试出这个bug可能会花费大量的时间,看下函数调用栈:
在我们简单的例子中,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
}
// 装配 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分为五个层次:
- 交互器(Interactor) — 包括关于数据和网络请求的业务逻辑,例如创建一个实体(Entities),或者从服务器中获取一些数据。为了实现这些功能,需要使用服务、管理器,但是他们并不被认为是VIPER架构内的模块,而是外部依赖。
- 展示器(Presenter) — 包含UI层面(但UIKit独立)的业务逻辑以及在交互器(Interactor)层面的方法调用。
- 实体(Entities) — 普通的数据对象,不属于数据访问层,因为数据访问属于交互器(Interactor)的职责。
- 路由器(Router) — 用来连接VIPER的各个模块。
基本上,VIPER的模块可以是一个屏幕或者用户使用应用的整个过程 —— 例如认证过程,可以由一屏完成或者需要几步才能完成。你想让模块多大,这取决于你。
当我们把VIPER和MV(X)系列作比较时,我们会在职责划分方面发现一些不同:
- Model(数据交互)逻辑以实体(Entities)为单位拆分到交互器(Interactor)中。
- 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
}
// 布局代码
}
// 装配 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架构还为时过早,考虑一些更为简单的模式反而会更好。一些人会忽略这些问题,大材小用。假定他们笃信VIPER架构会在未来给他们的应用带来一些好处,虽然现在维护起来确实是有些费劲。
总结
我们研究了几种架构模式,希望你能找到一些困扰你的问题的答案。但毫无疑问通过阅读这篇文章你应该已经认识到了没有绝对的解决方案。所以架构模式的选择需要根据实际情况进行利弊分析。
任何一种架构的方法,其本质不外乎,利用分、合、打散、重组等技术手段,对系统进行有序化重构,以达到减少系统“熵”的目的,减少系统内部混乱程度的过程,使系统得以不断进化。
因此,在同一应用程序中混合架构是很自然的。例如:你开始的时候使用MVC,然后突然意识到一个页面在MVC模式下的变得越来越难以维护,然后就切换到MVVM架构,但是仅仅针对这一个页面。并没有必要对哪些MVC模式下运转良好的页面进行重构,因为二者是可以并存的。