序言
之前看了一篇国外大牛Bohdan Orlov写的关于 iOS 架构模式的文章,内容涉及目前 iOS 端诸多主流的模式,个人感觉文章写的很不错,收获匪浅,希望能够通过翻译原文的方式更好的体会一下,也分享给更多的人参考。原文地址在这里,并附上相关PPT,浏览原文可能需要科学上网。
正文
在 iOS 开发中使用 MVC 是否感觉很怪异?对 MVVM 感到有疑问?听说过 VIPER,但是又不确定它是否有价值?继续阅读本文,你将会找到这些问题的答案,如果没有找到满意的答案,请在评论中随便吐槽吧。本文将帮助你建立起关于 iOS 端架构模式的知识体系。我们先来简要地回顾一些主流的架构,并且从理论和一些小例子的实践上进行比较。
注意:
学习设计模式是进阶阶段,因此在阅读本文之前,假设你已有一定的基础,不会再询问如下的问题:
1、谁应该持有网络请求,Model 还是 Controller?
2、如何给View的ViewModel传递Model?
3、谁能创建一个VIPER模块:Router 还是 Presenter?
为什么要关心选择什么样的架构
如果不关心架构,想象某天你调试一个巨大的类,里面有着数十个不同关联东西,你会发现几乎不可能定位问题点并修复bug。当然,你也很难去随心所欲地使用这个类,因为不了解类其中的一些重要细节。如果你在项目中已经遇到了这种场景,它可能是像这样的:
1、这个类是UIViewController的子类
2、数据直接存储在UIViewController中
3、视图View没有任何操作
4、Model的数据结构设计很糟糕
5、没有单元测试覆盖
即使你遵循了苹果指导意见并实现了苹果的 MVC 模式,这种情况还是可能会发生,不必觉得很难过。苹果的 MVC 有点问题,我们回头再说这件事情。让我们来定义一个好架构应该有的特征:
1、严格划分,均衡分配实体间的角色和职责
2、可测性通常是第一特性(不要担心:好架构一定具有可测性)
3、便于使用,且维护成本低
为什么要划分
当试图了解程序如何运行时,角色和职责划分能够让我们保持思路清晰。如果你的开发能力越强,你就越能理解复杂的事物。但是这种能力并不是线性增长的,会很快达到极限。因此降低复杂性的最简单办法是遵循单一责任原则,划分多个实体之间的职责。
为什么要可测性
对于那些由于添加新特性,或者一些正在重构中的错综复杂的类来说,开发人员应该感激出现失败的单元测试,因为这些失败的单元测试可以帮助开发人员尽快定位运行中出现的bug,而这些bug可能出现在用户的设备上,甚至需要花费数周才能修复。
为什么要易用
这不需要回答,但值得一提的是,最好的代码就是不用写代码,因此写的越少越不容易出错。这意味着想写少量代码的想法不仅仅是因为开发者的懒惰,而且你也不应当被一个更灵巧的解决方案所蒙蔽,而忽略了维护它的成本。
MV(X)系列导论
现在当我们要做架构设计时有很多种模式选择:
前三者采用的都是把App中实体划分成3类:
-
Models - 负责持有数据,进行数据处理的数据访问层。设想一下
Person
,PersonDataProvider
类。 -
Views - 负责数据展现层(Graphical User Interface),在iOS端可认为所有以
UI
前缀的类。 - Controller/Presenter/ViewModel - 负责协调处理Models和Views之间的交互。
通常用户操作视图会触发数据更新,数据的变更又会引起视图更新。这样的划分实体能让我们:
- 更好的理解他们是如何工作的
- 复用他们(通常可复用的是Views和Models)
- 单独测试他们
让我们开始学习MV(X)模式,稍后再说VIPER。
一、MVC(Model-View-Controller)
在讨论Apple版本的MVC之前,我们先来看看传统的MVC
图示中,视图Views是无状态的,它只是当数据Models发生变化时,通过Controller控制简单地展现一下。设想当你点击网页上某个跳转链接时,整个网页就会重新加载。虽然在iOS应用程序中这种传统的MVC很容易实现,但这是没有意义的,因为架构上3类实体紧密的耦合在一起,每一类实体都要和其他两类产生关联,这会大大降低代码的可复用性。这不会是你想要的架构,由于以上原因,我们就不写这种MVC的典型例子了。
传统的MVC不适合当前的iOS开发工作
Apple版的MVC
Apple期望的Cocoa MVC:
控制器Controller是视图Views和数据Models之间的中介,它们之间不需要有关联。可复用性最低的控制器Controller,通常是可以接受的,因为我们必须有一个地方来放置那些不适合放在数据Models中的所有复杂业务逻辑。理论上,它看上去非常简单明了,你是不是感觉到有什么问题?甚至听到过有人叫 MVC 为重控制器模式。此外,对于 iOS 开发者来说,给控制器减轻负担已经成为一个重要的话题。为什么苹果会采用仅仅改进过一点点的传统 MVC 模式呢?实际上的Realistic Cocoa MVC:
Realistic Cocoa MVC
Cocoa MVC鼓励你写重控制器是因为它们在Views的生命周期中相互依赖,以至于很难将它们分开。虽然你可能有办法把一些业务逻辑和数据转模型的工作放到Models中,但是对于分摊到Views上的工作却没有什么办法,大多数情况下,Views的所有功能就是给控制器Controller发送操作事件,而Controller最终会成为你可以想到所有东西的代理或者数据源,比如通常会负责发送或者取消网络请求等等。你经常会看到这样的代码:
var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell
userCell.configureWithUser(user)
cell 作为一个视图Views直接通过Models进行配置,MVC 的原则被违反了,但这种情况一直在发生,大家也没觉得有什么错。如果你严格的遵守 MVC,那么你就需要通过Controller对 cell 进行配置,并且不把Models传进Views中,然而这将会更进一步地增加Controller的规模。
把Cocoa MVC称作重控制器模式还是有一定道理的。
问题直到需要进行单元测试了才会暴露出来(希望你的项目也一样)。由于Controller和Views紧紧的耦合在一起,单元测试变得很困难,因为你将不得不非常有想象力的去模拟Views的生命周期,写Controller测试代码时也必须尽可能把业务逻辑代码同Views的布局代码分离开。让我们来看一个简单的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:", 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架构可以在视图控制器中进行组装
这段代码看上去不可测,对吧?我们可以把生成greeting
字符串的代码封装到新的GreetingModel
类中单独测试它。但是在不直接调用视图UIView
相关方法(viewDidLoad
,didTapButton
这些方法可能会加载所有视图)的前提下,我们还是无法测试GreetingViewController
内部任意的展现逻辑(虽然这个例子没有多少逻辑),这不利于单元测试。实际上,在一个模拟器(例如:iPhone 4S)上的测试并不能够保证在其他设备(例如:iPad)上也能运行良好。因此建议在单元测试中删除Host Application
的配置,并且测试用例不要运行在模拟器上。
视图和控制器之间的交互并不是真正的单元测试
综上所述,Cocoa MVC似乎是一个相当糟糕的模式。让我们用文章开头提到的好架构特征来对它进行一个评估:
- 划分 - View和Model确实是分离了,但是View和Controller还是紧紧地耦合在一起。
- 可测试性 - 由于划分的不好,你可能只能测试你的Model。
- 易用性 - 相比于其他模式代码量最小,此外门槛低,每个人都能熟练掌握,即使不是一个非常有经验的开发者也能进行维护。
如果对于你的小项目,不打算投入很多时间去设计架构,也不打算投入太多成本去维护,那么Cocoa MVC是你要选择的模式。
在开发速度上,Cocoa MVC是最好的架构模式。
二、MVP(Model-View-Presenter)
Cocoa MVC的演变
看上去是不是很像Cocoa MVC?的确很像,只是名叫MVP(Passive View Variant)。稍等。。。这是否意味着MVP的实质就是Cocoa MVC呢?当然不是,因为你回想一下View和Controller紧紧耦合在一起的位置,在MVP中是Presenter ,它与视图控制器的生命周期没有任何关联,并且由于没有任何布局的代码,很容易模拟视图View。它的职责是更新View中的数据和状态。
如果我告诉你
UIViewController
就是视图,会怎么样
在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()
}
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是第一个揭示了实际上由3个独立分层会存在组合问题的模式。既然我们并不希望View和Model耦合,在视图控制器中(实际上是View)组装它们就是不正确的,因此我们需要在其他地方处理。例如,我们可以让App范围内的路由服务来负责处理View与View之间的展现。这个问题不仅MVP中存在,后面将要介绍的所有模式中也都存在。我们来看看MVP的特征:
- 划分 - 大部分职责都被划分给了Presenter和Model,View没有任何职责(例子中的Model也没有职责)。
- 可测试性 - 很好,我们可以测试大部门业务逻辑,因为View无职责。
- 易用性 - 在我们那个的不切实际的例子中,代码量比MVC翻了一倍,但同时,MVP的设计思路非常清晰。
在iOS开发中使用MVP模式意味着良好的可测性和很多的代码量。
基于绑定和监控
还有另外一种形式的MVP模式 — 带监控器的MVP。这种模式的特点包括直接绑定View和Model,同时Presenter(监控器)仍然控制着View上的操作事件,并能改变View的展现。
但正如我们之前认识到的,模糊不清的职责分配是不好的设计,View和Model也紧紧的耦合在一起。这种模式跟Cocoa桌面端程序开发相似。和传统的MVC模式一样,对于有缺陷的架构,我认为没有必要再举例。
三、MVVM(Model-View-ViewModel)
MVVM是最新的MV(X)系列架构,我们希望它在设计之初就已经考虑到之前的MV(X)系列所面临的问题。从理论上来看,Model-View-ViewModel看起来不错。View和Model我们已经很熟悉了,但中间层换成了ViewModel。
它和MVP模式很像:
- 视图控制器划分成View
- View和Model之间没有紧密的耦合
此外,数据绑定的概念很像带监控器的MVP,不同的是这次绑定的是View和ViewModel,而不是View和Model。那么在实际的iOS开发中ViewModel是什么?从根本上来说,它是独立于UIKit
能够展现你的View和状态。ViewModel可以调用Model来改变数据,也可以通过数据变更来更新自己,因为View和ViewModel进行了绑定,相应地也就能同步更新View。
绑定
在介绍MVP部分我简要地提到绑定的概念,但我们还是在这里讨论一下它。绑定来源于OS X开发环境,在iOS开发中没有。当然我们可以使用KVO
和消息通知机制,但都不如绑定方便。因此,如果我们不想自己实现一套绑定机制,有两种选择:
- 基于
KVO
的数据绑定库,比如RZDataBinding,SwiftBond - 全量级的函数式响应编程框架,比如ReactiveCocoa,RxSwift,PromiseKit
实际上,当你听到MVVM就会联想到ReactiveCocoa
,反之亦然。虽然使用ReactiveCocoa
框架可能是你很容易建立起基于绑定的MVVM,并且发挥出它的最大价值,但响应式框架有一个痛苦的现实:能力越大,责任也就越大。当你使用响应式框架的时候很容易就搞得乱七八糟,换句话说,如果出现bug,你将会花费大量的时间去调试bug,看看下面的调用堆栈图。
在我们的简单例子中,无论是使用函数响应式框架,还是KVO
都有点大材小用。我们换另外的方式,通过调用showGreeting
方法来请求View Model更新View,使用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的特征:
- 划分 - 在我们这个小例子中看到的不是很清晰,但实际上,MVVM中的View承担了比MVP要多的职责。首先它需要绑定ViewModel来更新状态,其次它需要传递所有的事件消息而不需要更新事件的提供者。
- 可测试性 - ViewModel并不持有View,这让我们很容易测试它。View也可以测试,但它依赖
UIKit
通常会忽略掉。 - 易用性 - 代码量和MVP一样多,但真实的App开发中如果采用绑定机制,去替换那些传递事件和手动更新的代码,会减少很多代码量。
MVVM是非常吸引人的,因为它结合了前面提及的几种模式的优点,此外使用绑定机制不需要编写额外的视图更新代码,并且保持了良好的可测试性。
四、VIPER(View-Interactor-Presenter-Entity-Routing)
从搭积木中领悟的iOS设计
VIPER是本文最后一个介绍的架构模式,它很有趣,不属于MV(X) 系列的扩展。到目前为止,你必须意识到一个好的设计一定有细粒度的职责划分。VIPER从另一个不同的角度进行了职责划分,这次我们分为5层:
-
交互器:包含与数据(实体)或网络相关的业务逻辑,比如从服务器获取一些新的数据实体,为了这些目的,你会使用一些
Services
和Managers
,它们并不被认为属于VIPER中的一部分,更确切地说它们是一种额外的依赖。 -
展示器:包含一些与UI相关(
UIKit
除外)的业务逻辑,通过交互器调用方法。 - 实体:纯粹的数据对象,不包含数据访问层,因为这是交互器的职责。
- 路由器:负责VIPER模块之间的切换。从根本上说,粒度划分方式,VIPER模块可以用来设计一个场景的功能,也可以用来设计应用中的一个完整用户故事—比如身份验证,是由一个场景或者若干场景组成,应该用多大的积木块来搭乐高玩具,完全取决于你。
同MV(X)系列对比,我们会发现在职责划分上有一些不同点:
- Model — 数据交互逻辑被转移到了交互器Interactor中,Entities只有纯粹的数据结构。
- Controller/Presenter/ViewModel中的UI展现职责转移到了交互器Interactor中,但它没有更改数据的能力。
- VIPER是第一个明确提出地址导航职责应该由路由器Router来解决。
在iOS应用中找到一种合适的路由方式是一个挑战,MV(X)系列模式都没有定位到这个问题。
下面的VIPER例子中没有涉及到模块之间的路由或交互,当然在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在职责划分上是最好的。
- 可测试性 - 毫无悬念,好的职责划分必然有好的可测性。
- 易用性 - 由于上述两个特征你就可以猜测到代码维护性成本很高,你不得不编写大量的接口类来完成很小的职责。
总结
我们已经从头到尾地了解了几种架构模式,希望你能从中找到那些曾经困扰你很久的问题的答案。但我毫不怀疑,你已经意识到了没有什么银色子弹,选择什么样的架构设计是特定场景下权衡各种因素之后的结果。因此,在同一个app中就会出现混合架构设计。比如:一开始使用MVC,然后你发现有一些特殊场景如果使用MVC将会难以维护,这时你可以仅对这个场景使用MVVM模式,没必要去重构那些MVC架构执行的很好的模块。MV(X)系列是互相兼容的。
Make everything as simple as possible, but not simpler. -- Albert Einstein
网友评论
你确定????
这里有错误,应该是转移到展示器Presenter中,Presenter没有直接和数据交互的能力
1、谁应该持有网络请求,Model 还是 Controller?
2、如何给View的ViewModel传递Model?
3、谁能创建一个VIPER模块:Router 还是 Presenter?
解决这三个问题,应该学习哪方面的知识,或者说搜索哪些关键字。目前只知道,M,V,C.
ViewModel, VIPER,Router,Presenter 这些单词没怎么听过都。