如果你在iOS平台上有一定开发经验,一定听说过Model-View-Controller,或称MVC模式,MVC是构建iOS应用的标准设计模式。但实际开发中,MVC暴露出许多的缺陷。
通过这篇文章,我会介绍什么是MVC,以及MVC的缺陷。对于如何解决MVC的这些缺陷,我会介绍一种全新设计模式:Model-View-ViewModel,即MVVM。
1 MVC模式及其缺陷
1.1 MVC模式简介
苹果官方称MVC设计模式(简称MVC)是iOS应用设计的最佳模式。
在MVC中,任何对象都可被归为模型对象、视图对象或控制器对象。模型对象(Model Object,简称模型)持有程序数据;视图对象(View Object,简称视图)负责用户交互;控制器对象(Controller Object,简称控制器)则作为模型和视图沟通的媒介。
视图将用户输入转交给控制器,由控制器按照相应请求通知模型进行更新,模型将自身数据变化通知(通常通过KVO的途径)控制器,最后控制器操作视图进行相应的更新(如下图所示):
模型通常十分简洁,大多数情况下它们由Core Data管理。根据苹果的描述,模型中封装了数据以及对数据的操作代码。实际工作中使用的模型对象通常都很小,但数据操作代码时常会被耦合到控制器中。
视图是UIKit对象或自定义对象,它是程序中的可视部分,通常存在于.xib文件或storyboard中,比如按钮或标签。视图不应同模型直接交互,而只能通过控制器的IBAction事件进行间接交互,并且非视图逻辑不应写在视图中。
控制器相当于胶水代码,将模型和视图粘合在一起。控制器负责控制视图的加载,显示,隐藏等。在控制器中还存在控制模型的代码,以及那些既不属于模型对象又不属于视图对象的代码,从而引出MVC模式的第一个缺陷。
1.2 MVC的缺陷
1.2.1 臃肿的控制器
控制器的功能决定了其内部既包含视图控制又包含模型控制,这使得控制器十分臃肿。
千上万行代码挤在一个控制器内,导致代码不易维护。比如一个控制器有数十个属性,要管理它的各种状态就十分困难。再比如一个控制器接受了若干协议,那其中就可能既包含协议方法代码,又包含控制器自身的控制逻辑。
另外一个结果是程序不易测试(无论是手动测试还是单元测试),因为控制器拥有太多不同的状态,无法将所有状态都覆盖到。
1.2.2 网络逻辑无处可放
如果程序中所有对象都被归入MVC,那么问题来了:网络代码应归为哪一类?API通信代码又归为哪一类?
假设网络代码被放在模型中,由于网络请求必须支持异步完成,如果模型已经消失,而网络请求还在继续,那这个问题就复杂了。
另外,网络代码不可能放在视图中,那就只剩一个地方了:控制器。但把网络代码放在控制器中的做法很糟糕,这样会加剧之前提到的问题:臃肿的控制器。
那么,到底该将网络代码放在何处?MVC中并没有提供解决方案。
1.2.3 代码难以测试
MVC另外一个问题是它不利于测试。控制器中混合了操作逻辑和业务逻辑,想要将这二者分离进行测试会十分困难。当然,许多人都有办法忽略这个问题,即不进行任何测试。
1.2.4 模糊的管理
控制器管理着一个视图树,通过IBOutlet还可以访问任意子视图,但当使用控制器管理大量子视图时,用outlet来访问就显得过于复杂了,一个解决办法是添加子控制器管理子视图。
无论怎么做,控制器和对应的视图都紧密耦合,当然也可以把它们二者就看做一个整体。
不过,可以看看下面这个办法。
2 MVVM模式
也许在理想状态下MVC会有很好的效果,但现实中它却不尽如人意。
我们已经对MVC模式有了详细了解,下面就介绍它的替代方案:Model-View-ViewModel(MVVM)模式。
MVVM由微软公司提出(先不要对它产生偏见),它和MVC模式有许多相同之处。MVVM将视图和控制器的耦合部分抽取出来,从而引入一个新的部分,即View-Model(VM)。
在MVVM模式下,视图及控制器形式上关联在一起,故可将二者视为一个整体。视图仍不会同模型直接交互,但控制器也没有同模型直接交互,取而代之的是视图模型(ViewModel)。
诸如验证用户输入请求、视图的显示逻辑、剥离网络请求或其他类似代码,将它们放在视图模型中再合适不过了,并且视图模型中没有对视图的直接引用。
视图模型中封装的代码既可以工作在iOS下,也可以工作在OSX下(换句话说就是不要在视图模型中引入和具体平台有关的内容,如#import UIKit.h)。
由于将视图逻辑——比如说将模型中的某些值映射为一个字符串的操作,都放在了视图模型中,故大大降低了控制器的复杂程度和它与其他部分的耦合程度。
使用MVVM的最大好处是,你可以先放入一小部分逻辑到视图模型中,随着对模式了解的深入,慢慢再将其他代码迁移到其中。
MVVM模式对测试十分友好。由于视图模型中包含了所有的显示逻辑,并且没有任何对于视图的直接引用,故完全可以自动化测试,并且使用MVVM的程序可以充分地运行单元测试。
就我使用MVVM的经验来看,这种模式会造成代码量稍稍提升,但是代码复杂程度得到极大降低,孰优孰劣一目了然。
本文中多次在不同地方使用到了“通知”和“更新”,但没有说明如何进行这样的操作。你可以在MVC模式中使用KVO来进行这样的操作,但代码会很快变得难以管理。实际工作中,更倾向于在MVVM中使用ReactiveCocoa来将所有的部分关联到一起。