前段时间,在使用了一段时间的MVVM架构之后,我从实际的项目中抽离出来,对使用MVVM架构的整个过程进行了总结,对于架构、对于编程思维又有了不一样的体会。于是提笔写下自己探索MVVM架构的经验和心得,以飨读者。
本文会先对MVC架构做一个回顾,明确MVC中各层的职责;然后会提出MVVM架构的概念,本来接下来应该顺势举几个MVVM的例子进行说明的,但是考虑到响应式编程之于MVVM的重要性,所以在举例之前会先讲解一下响应式编程的概念(出于篇幅考虑,将MVVM架构实践独立成一篇文章,想直接看实例的请移驾这里);最后会对MVC和MVVM的取舍谈谈自己的看法。话不多说,现在进入正题。
MVC架构
MVC(Model-View-Controller),是一种常见的客户端软件开发框架,具体到iOS上,绝大部分人从开始接触iOS编程的时候都被告知MVC就是事实上的默认框架。系统也为我们实现好了公共的视图类:UIView 和控制器类:UIViewController。大多数时候,我们都需要继承这些类来实现我们的程序逻辑,因此,我们几乎逃避不开MVC这种设计模式。下面就对MVC各层的职责进行明确:
Model
Model层 是服务端数据在客户端的映射,是薄薄的一层,完全可以用struct表征。下面看一个实例:
可以看到,Model 层通常是服务端传回的 JSON数据的映射,对应为一个一个的属性。不过现在也有很多人将网络层(Service层)归到Model中,也就是MVC(S)架构。同时,大部分时候数据的持久化操作也会放在Model层中。
总结一下,Model层的职责主要有以下几项:HTTP请求、进行字段验证、持久化等。
View层
View层是展示在屏幕上的视图的封装,在 iOS 中也就是UIView以及UIView的子类。下面是UIView的继承层级图:
View层的职责是展示内容和接受用户的操作与事件。
Controller层
看了Model层和View层如此简单清晰的定义,如果你以为接下来要讲的Controller层的定义也跟这两层一样,那你就要失望了。
粗略总结了一下,Controller层的职责包括但不限于:管理根视图以及其子视图的生命周期、展示内容和布局、处理用户行为(如按钮的点击和手势的触发等)、储存当前界面的状态(例如分页加载的页数、是否正在进行网络请求的布尔值等)、处理界面的跳转、作为UITableView以及其它容器视图的代理以及数据源、业务逻辑和各种动画效果等。
画风似乎不对啊,为什么Controller层的职责比其他两层加起来还多?
MVC的困境
因为MVC架构中Controller层往往代码很多,动辄2、3千行的这一特点,MVC也常常被调侃成是 Massive View Controller。造成这个问题的原因就是MVC的定义太过简单朴素,要知道支撑一个尚不算大的企业级应用都动辄几十万行代码,还不包括各种依赖的第三方库。这么多的代码如何安置?按照传统的MVC定义,分割了小部分到Model层和View层,剩下的代码都没有其他地方可以去了,于是被统统的丢到了Controll层中。
庞大的Controller层带来的问题就是难以维护、难以测试。而且其中充斥着大量的状态值,一个任务的完成依赖于好几个状态值,而一个状态值又同时参与到多个任务中,这样复杂的多对多关系带来的问题就是开发效率低下,需要花费大量的时间周旋在各个状态值之间,对以后的功能拓展、业务添加也造成了障碍。
这样的前提下,架构的改进就显得非常有必要了。
MVVM架构初探
MVVM(Model-View-ViewModel),2005年由微软的WPF和Silverlight的架构师 John Gossman 提出,是MVP模式与WPF结合发展演变过来的一种架构框架。MVVM实质上还是MVC架构范围,是一个精心优化的MVC架构,所以与MVC架构是兼容的。
MVVM首先将View层和Controller层进行了合并,统称为View层,因为View层和Controller层往往是一起出现的。然后引入了一个新的模块 — ViewModel层,ViewModel层承载的内容就是之前在Controller层中视图展现逻辑。MVVM的图示如下:
什么是视图展现逻辑呢?在一款应用中,数据的来源可能是服务端返回、数据库获取和用户输入,然后存储在Model中,但是这样的数据是一种“未经格式化的”原始数据,还不能直接显示到屏幕上。比如Model中可能有姓、名、昵称等属性,在某些界面中需要显示成”姓名”的样式,某些界面中显示成”名姓”的样式,某些界面中显示”昵称”的样式。视图展现逻辑就是把这些原始数据经过业务需求处理成展现到屏幕上的数据。可以把一个应用看成是播出一个新闻节目,Model层就是一大堆繁杂的稿件,View层就是主持人实际播报的新闻,而ViewModel层就是幕后的编辑处理团队,负责从凌乱的稿件中抽出需要的信息,整理成播报时用的稿件。这样主持人拿着整理好的稿件,就能轻松的播报新闻了。
但是呢,平白无故多了一个ViewModel层。多一个层带来的直接问题就是信息的传递问题,层与层之间需要互通信息,进行交流。在MVVM架构的实现中,开发人员想出了一个与传统消息传递所不一样的方式,这就引出了响应式编程的概念。
响应式编程
响应式编程(Reactive Programming),是一种面向数据流和变化传播的范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。举个维基百科中的例子:c:=a+b表示将表达式的结果赋给c,而之后改变a或b的值不会影响c。但在响应式编程中,c的值会随着a或b的更新而更新。
也就是说,上图中c的值最后会是5。
同样的例子还有Excel中的单元格,单元格可以包含字面值或类似”=B1+C1”的公式,而包含公式的单元格的值会依据其他单元格的值的变化而变化 。
如何实现所谓的响应式编程?在WPF中官方提供了Data Binding技术,macOS中也有类似的Cocoa Binding框架,但是在iOS中官方没有提供这样的框架。于是GitHub上出现了ReactiveCococa(以下简称为RAC)和RxSwift等优秀的第三方框架。
在RAC的思维中,iOS上的一切都是在变化的数据流,比如输入框上用户正在不断输入的文字、被点击的按钮、旋转缩放的视图、不断改变的NSString等等,这些就像是一个”水龙头”,当有变化产生的时候,水龙头就会出水,把变化传递下去,对这个变化感兴趣的人就可以在这个水龙头上套一个”水管”,这个人就成为了一个接收者(subscriber),当有变化产生的时候,接收者就能从水管中拿到这个变化的具体信息。
RAC就提供了这样的”水管”,但是和现实中的水管有所不同,RAC有自己的一些限制:水管中传递的不是水,而是一个个的”玻璃球”,这些玻璃球的直径和水管的内径一样大,保证了玻璃球在水管中都是依次排列通过的,这就保证了不会出现多个玻璃球并列通过的情况。更加重要的是,在拿到玻璃球之前,可以对其进行一些个性化的定制。例如,可以在水龙头上加一个过滤嘴(filter),不符合的不让通过;也可以加一个改动装置,把球改变成符合自己的需求(map);还可以把多个水龙头合并成一个新的水龙头(combineLatest:reduce:),经过了这些定制之后,出来的符合要求的玻璃球就能拿来直接用了。
在这么强大的框架帮助下,MVVM所引入的ViewModel层和其他层之间的通讯问题得到了解决。
MVC还是MVVM?
在考虑是否要选择MVVM架构之前,先来总结一下MVVM的优势和不足。
MVVM主要有以下几个优势:
- Controller层瘦身:将视图展现逻辑抽出到ViewModel层带来的直接变化就是Controller层变得更加轻量级,更加容易维护。
- 更加容易测试:Controller层代码减少了,也意味着对Controller层的测试更加容易。
- 兼容MVC:选择MVVM并不意味着完全摒弃MVC,MVVM相当于是MVC的超集,所以和MVC是兼容的,也就是说,可以只在某一个模块中使用MVVM,不用担心迁移架构时会造成需要全局重构的问题。
- 解决状态以及状态之间依赖过多的问题:这个优势是由RAC所带来的,响应式编程关注的数据的变化和流向,免除了一部分的状态,而是直接将变化传到显示的控件上,实例在这里。
- 提供统一的消息传递机制:这也是由RAC所带来的,RAC对iOS编程中大部分的实物进行了抽象,提供了统一的接口,所以可以将iOS上KVO、通知(NSNotification)、委托(delegate)、Target-Action、块(Block)等消息传递方式统一,实例在这里。
MVVM存在的问题主要有:
- 学习曲线比较陡,通常需要引入第三方库(ReactiveCocoa/RxSwift):使用MVVM通常需要引入第三方库,而且需要转换成响应式编程的思维方式,这是需要花费相当的学习适应时间的。
- 创建更多的类:基本上每个Controller类会对应有一个ViewModel类
- 性能上有一定影响,调用栈变深:RAC的实现底层依赖于KVO,带来的问题是性能的损耗,比如光是subscribNext就慢了1个数量级,目前的回调堆栈也比较深,最简单的[signal subscribeNext^(id x){}]就会有近40次的调用。
MVVM好处不少,缺点也一堆。那到底要不要用MVVM呢?我觉得,在项目还不算臃肿的时候,可以简单的对现有的MVC进行解耦优化,比如将网络层(Service层)、持久层(Storage层)等部分抽象出来即可。另一方面,MVVM对MVC也是兼容的,可以考虑在项目中的某个模块试水MVVM,觉得好再逐步替换其他模块;而且很重要的一点是,响应式编程这样一种范式相当的锻炼我们的编程思维,让我们可以站在数据的变化和流向的角度去思考我们的整一个项目,掌握这种思维方式也可以反哺到我们项目中别的地方。
架构没有绝对的优劣,适合自己的架构就是最好的架构,那就让我们理性分析,拥抱变化。