序言
为什么要关注架构设计?
因为假如你不关心架构,那么总有一天,需要在同一个庞大的类中调试若干复杂的事情,你会发现在这样的条件下,根本不可能在这个类中快速的找到以及有效的修改任何bug.当然,把这样的一个类想象为一个整体是困难的,因此,有可能一些重要的细节总会在这个过程中会被忽略。
架构模式
一个整体的分层? 逻辑清晰? 还是清晰的分工? 对于架构模式来说并没有一个非常明确的定义, 比较抽象, 在于设计在于架构, 不管是小到类与类之间的交互, 还是不同的小模块, 小版块之间, 甚至于在不同的业务之间, 我们都可以去从架构的方面去理解, 去分析。
那么在iOS中常见的架构来说, 常见有就有这些: MVC架构, MVP架构, MVVM架构
当然这些架构都有一个共同特点: 解耦合
MVC
什么是MVC?
MVC最早存在于桌面程序中的, M是指业务数据, V是指用户界面, C则是控制器. 在具体的业务场景中, C作为M和V之间的连接, 负责获取输入的业务数据, 然后将处理后的数据输出到界面上做相应展示, 另外, 在数据有所更新时, C还需要及时提交相应更新到界面展示. 在上述过程中, 因为M和V之间是完全隔离的, 所以在业务场景切换时, 通常只需要替换相应的C, 复用已有的M和V便可快速搭建新的业务场景. MVC因其复用性, 大大提高了开发效率, 现已被广泛应用在各端开发中。
MVC的过去
在我们探讨Apple的MVC模式之前,我们来看下传统的MVC模式。
在图里,View并没有任何界限,仅仅是简单的在Controller中呈现出Model的变化。想象一下,就像网页一样,在点击了跳转到某个其他页面的连接之后就会完全的重新加载页面。尽管在iOS平台上实现这这种MVC模式是没有任何难度的,但是它并不会为我们解决架构问题带来任何裨益。因为它本身也是,三个实体间相互都有通信,而且是紧密耦合的。这很显然会大大降低了三者的复用性,而这正是我们不愿意看到的。
然而传统的MVC架构不适用于现在的iOS开发
苹果推荐的MVC---愿景
由于Controller是一个介于View 和 Model之间的协调器,所以View和Model之间没有任何直接的联系。Controller是一个最小可重用单元,这对我们来说是一个好消息,因为我们总要找一个地方来写逻辑复杂度较高的代码,而这些代码又不适合放在Model中。理论上来讲,这种模式看起来非常直观,但你有没有感到哪里有一丝诡异?你甚至听说过,有人将MVC的缩写展开成(Massive View Controller),更有甚者,为View Controller减负也成为iOS开发者面临的一个重要话题。如果苹果继承并且对MVC模式有一些进展,所有这些为什么还会发生?
苹果推荐的MVC--事实
Cocoa的MVC模式驱使人们写出臃肿的视图控制器,因为它们经常被混杂到View的生命周期中,因此很难说View和ViewController是分离的。尽管仍可以将业务逻辑和数据转换到Model,但是大多数情况下当需要为View减负的时候我们却无能为力了,View的最大的任务就是向Controller传递用户动作事件。ViewController最终会承担一切代理和数据源的职责,还负责一些分发和取消网络请求以及一些其他的任务。
你可能会看见过很多次这样的代码:
- varuserCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell
- userCell.configureWithUser(user)
这个cell,正是由View直接来调用Model,所以事实上MVC的原则已经违背了,但是这种情况是一直发生的甚至于人们不觉得这里有哪些不对。如果严格遵守MVC的话,你会把对cell的设置放在 Controller 中,不向View传递一个Model对象,这样就会大大增加Controller的体积。
直到进行单元测试的时候才会发现问题越来越明显。因为你的ViewController和View是紧密耦合的,对它们进行测试就显得很艰难--你得有足够的创造性来模拟View和它们的生命周期,在以这样的方式来写View Controller的同时,业务逻辑的代码也逐渐被分散到View的布局代码中去。
我们看下一些简单的例子:
- import UIKit
- struct Person { // Model
- let firstName: String
- let lastName: String
- }
- class GreetingViewController : UIViewController { // View + Controller
- varperson: 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;
这段代码看起来可测试性并不强,我们可以把和greeting相关的都放到GreetingModel中然后分开测试,但是这样我们就无法通过直接调用在GreetingViewController中的UIView的方法(viewDidLoad和didTapButton方法)来测试页面的展示逻辑了,因为一旦调用则会使整个页面都变化,这对单元测试来讲并不是什么好消息。事实上,在单独一个模拟器中(比如iPhone 4S)加载并测试UIView并不能保证在其他设备中也能正常工作,因此我建议在单元测试的Target的设置下移除"Host Application"项,并且不要在模拟器中测试你的应用。
以上所述,似乎Cocoa MVC 看起来是一个相当差的架构方案。我们来评估一下MVC一系列的特征:
- 任务均摊--View和Model确实是分开的,但是View和Controller却是紧密耦合的
- 可测试性--由于糟糕的分散性,只能对Model进行测试
- 易用性--与其他几种模式相比最小的代码量。熟悉的人很多,因而即使对于经验不那么丰富的开发者来讲维护起来也较为容易。
如果你不想在架构选择上投入更多精力,那么Cocoa MVC无疑是最好的方案,而且你会发现一些其他维护成本较高的模式对于你所开发的小的应用是一个致命的打击。
MVP
MVC的缺点在于并没有区分业务逻辑和业务展示, 这对单元测试很不友好. MVP针对以上缺点做了优化, 它将业务逻辑和业务展示也做了一层隔离, 对应的就变成了MVCP. M和V功能不变, 原来的C现在只负责布局, 而所有的逻辑全都转移到了P层。
MVP实现了Cocoa的MVC的愿景
这看起来不正是苹果所提出的MVC方案吗?确实是的,这种模式的名字叫做MVC,但是,这就是说苹果的MVC实际上就是MVP了?不,并不是这样的。如果你仔细回忆一下,View是和Controller紧密耦合的,但是MVP的协调器Presenter并没有对ViewController的生命周期做任何改变,因此View可以很容易的被模拟出来。在Presenter中根本没有和布局有关的代码,但是它却负责更新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 {
- varpresenter: 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的概念却非常清晰
MVP--绑定和信号
还有一些其他形态的MVP--监控控制器的MVP。
这个变体包含了View和Model之间的直接绑定,但是Presenter仍然来管理来自View的动作事件,同时也能胜任对View的更新。
但是我们之前就了解到,模糊的职责划分是非常糟糕的,更何况将View和Model紧密的联系起来。这和Cocoa的桌面开发的原理有些相似。
MVVM
MVVM架构是MV(X)系列最新的一员,因此让我们希望它已经考虑到MV(X)系列中之前已经出现的问题。从理论层面来讲MVVM看起来不错,我们已经非常熟悉View和Model,以及Meditor,在MVVM中它是View Model。
它和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可能会花费大量的时间,看下函数调用栈:
在我们简单的例子中,FRF框架和KVO被过渡禁用,取而代之地我们直接去调用showGreeting方法更新ViewModel,以及通过greetingDidChange 回调函数使用属性。
- import UIKit
- struct Person { // Model
- let firstName: String
- let lastName: String
- }
- protocol GreetingViewModelProtocol: class {
- vargreeting: String? { get }
- vargreetingDidChange: ((GreetingViewModelProtocol) -> ())? { get set } // function to call when greeting did change
- init(person: Person)
- func showGreeting()
- }
- class GreetingViewModel : GreetingViewModelProtocol {
- let person: Person
- vargreeting: String? {
- didSet {
- self.greetingDidChange?(self)
- }
- }
- vargreetingDidChange: ((GreetingViewModelProtocol) -> ())?
- required init(person: Person) {
- self.person = person
- }
- func showGreeting() {
- self.greeting = "Hello"+ " "+ self.person.firstName + " "+ self.person.lastName
- }
- }
- class GreetingViewController : UIViewController {
- varviewModel: 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中的View承担的责任多。因为前者通过ViewModel的设置绑定来更新状态,而后者只监听Presenter的事件但并不会对自己有什么更新。
- 可测试性 -- ViewModel不知道关于View的任何事情,这允许我们可以轻易的测试ViewModel。同时View也可以被测试,但是由于属于UIKit的范畴,对他们的测试通常会被忽略。
- 易用性 -- 在我们例子中的代码量和MVP的差不多,但是在实际开发中,我们必须把View中的事件指向Presenter并且手动的来更新View,如果使用绑定的话,MVVM代码量将会小的多。
总结
我们了解了集中架构模式,希望你已经找到了到底是什么在困扰你。毫无疑问通过阅读本篇文章,你已经了解到其实并没有完全的银弹。所以选择架构是一个根据实际情况具体分析利弊的过程。
因此,在同一个应用中包含着多种架构。比如,你开始的时候使用MVC,然后突然意识到一个页面在MVC模式下的变得越来越难以维护,然后就切换到MVVM架构,但是仅仅针对这一个页面。并没有必要对哪些MVC模式下运转良好的页面进行重构,因为二者是可以并存的。