前言
很早前就想聊聊 MVC、MVVM,因为这是个非常有意思的话题,而且近些年来新的架构设计模式也层出不穷,除却 MVVM,还有 VIPER、ELM 等,如果不深入探究一下,很容易给人一种烟花渐欲迷人眼的错觉,没事,正如鲁迅曰过的,"一切的恐惧都源自于无知",(鲁迅:我说的?黑人❓),所以,掌握了整个事物的本质,也就多拥有了几分美好与泰然。
直觉告诉我,这应该会是个系列文章,今天就先着重说说 MVC 的那些事。
关于 MVC 的那些事
我们首先说说 MVC,MVC 这个概念最早是由 Tryge Reenskaug (施乐帕克实验室)于1979 年提出来的,原本是用它来描述 Small Talk 中的 "Template Pattern" 应用的,而 Cocoa 中的 MVC 的实现可以追溯到 1997 年左右的 NeXTSTEP 4 的时代。
在 iOS 开发中,不管是什么架构模式,基本上都是基于 MVC 演进而来。MVC 是 iOS 开发中使用最普遍的架构模式,同时也是苹果官方推荐的架构模式。
有着正统的历史,有着苹果的背书,那到底是哪里出了问题,导致 MVC 被人诟病不断从而不断提出新的架构设计方案呢?带着问题,我们来一步步深入了解下 MVC,答案自然会揭晓。
苹果所设想的 MVC 架构
我们先来看一下苹果所期望的 MVC 架构设想图:
基于上图,我简要说明一下几点:
- Controller 在编译期对 View 和 Model 都有强引用,知道自己所连接的的 View 和 Model 的接口,而 View 和 Model 对 Controller 是弱引用,即是在运行时才知道自己的监听者是谁。换句话说,即 View 和 Model 不依赖于 Controller,Model 和 View 之间也不相互依赖,提高了可复用性。
- Controller 做的事情,就是调度 Model 与 View 之间的关系。说的简单些,就是把 Model 提供的数据丢给 View 去展示,监听 View 产生的用户事件去改变 Model 数据。
一个完整的流程大致如下,我们从一个用户事件开始:
用户点击 View 产生事件,View 发送 action 给 Controller,Controller 接收到事件后更改 Model,而 Model 的数据被更改后会通知监听者自己的变化(这时 Model 的监听者就是 Controller),Controller 检测到 Model 变化后,就使用最新的数据刷新 View 。至此,完成了一个完整的事件流传递过程。
我们从这个过程中看到了,数据的传递是以单向数据流流转的,而且 M、V、C 各自职责分明,C 负责调配 View 与 Model,Model 负责数据封装,View 负责展示数据,看似完美的架构方式,会有什么问题吗?
开发者实际使用的 MVC
鲁迅常说,"理想很丰满,现实很骨感"(鲁迅:又是我说的❓❓),下面就说一下,苹果的这种 MVC 设想方式,在落地实践的时候有哪些问题。
问题一:View?Controller?ViewController?傻傻分不清楚
这个问题的由来,源自于苹果本身的 Cocoa 设计。按照 Cocoa 的设计,总给人一种此 ViewController 非彼 Controller 的感觉,为什么这么说呢?
其一, ViewController 但从名字来看,就是好像是被设计为 View 的 Controller 而非 MVC 中的 C;
其二,ViewController 连 View 的整个生命周期都托管了,viewDidLoad
、viewWillAppear
、viewDidAppear
、viewXXX
。
种种迹象表明,按照苹果 Cocoa 的设计,View 跟 Controller 有一种扯不断理还乱的关系。仿佛变成了下面这幅图:
问题二:Controller 如果使用不当,很容易变成胖 Controller。
有人调侃,苹果的 MVC 其实就是 Massive ViewController。为什么容易变胖呢?好好活着不行吗?原因主要如下:
Controller 除了承担他本应承担的调配 View 跟 Model 的相互协调的工作外,还要加工组装数据,还承担了 View 相关的工作,再到后面繁重的业务逻辑一来的时候,都一股脑全塞给了 Controller。至此,Controller 彻底沦陷。Game Over。
这个问题大家应该可以感同身受,所以就不必多解释了。
问题三:过于强调 Model 与 View 的隔离,会让 View 层的封装性被破坏。
这一个问题可能大家还没反应过来是什么意思,这样,我先贴出一段代码,大家应该很快就能 Get 到我要表达的意思。
// UITableViewCell.h
@interface MCPostListCell : UITableViewCell
@property (nonatomic, strong) UIImageView *avatarView;
@property (nonatomic, strong) UILabel *nickLabel;
@property (nonatomic, strong) UILabel *ageLabel;
@property (nonatomic, strong) UILabel *groupLabel;
@property (nonatomic, strong) UILabel *locationLabel;
@property (nonatomic, strong) UILabel *signLabel;
...
...
...
@end
// Controller.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MCPostListCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellID];
MCPostFloorModel *model = _dataSource[indexPath.item];
[cell.avatarView sd_setImageWithURL:model.xxx];
cell.nickLabel.text = model.xxx;
cell.ageLabel.text = model.xxx;
cell.groupLabel.text = model.xxx;
cell.locationLabel.text = model.xxx;
cell.signLabel.text = model.xxx;
...
...
...
return cell;
}
这段代码,从 Cocoa 的设计规范上讲,没有什么可苛责的。毕竟 View 和 Model 不能产生关系,所以 View 只能通过暴露自己的接口给 Controller,让 Controller 从 Model 取值后去给 View 赋值。
但是直觉告诉我们,一个 TableViewCell 暴露出了 30 多个属性给 Controller 肯定是哪里出了问题,不太符合面向对象的设计规范,暴露出太多细节,这个 View 的会变得非常脆弱,容易被外部搞得一团糟,而且如果外部想渲染出自己想要的 View,还需要去关心 View 内部的实现细节,这显然不是我们想要的结果。
Apple 设计的 MVC 真的是那么不堪吗?
看到前面说到的 MVC 存在的几个问题,肯定会很疑惑,真的有那么不堪吗?
当然不是。
上面的问题,有的是源自于开发者对苹果 Cocoa 设计的不够深入或者理解偏差导致的,有的是我们可以对 MVC 稍微做下变体就能解决。
下面我们针对上面的问题一一给出回答。
解答问题一:提问:"View?Controller?ViewController?傻傻分不清楚"
其实,这个问题,我们误解了苹果。我先说出我的想法,ViewController 和 View 都同属于 C 的范畴。注意我这里说的 View 是控制器的 View。为了不引起歧义,后面统一把控制器的 View 称为 ContainerView。看下下面的图,应该就能深有体会。
解答问题二:提问:"Controller 如果使用不当,很容易变成胖 Controller"
关于这个问题,在给出方案之前,我先抛个反问:“你认真读过《代码整洁之道》吗?”
诚然,MVC 架构中的 C 容易变胖,不光 iOS,Android 中的 Activity 也会不经意间慢慢变胖。但是扪心自问一下,往 ViewController 里面塞代码的时候,有认真思考过这些代码是不是应该放在这里吗?
答案恐怕是,没认真思考过。"一段代码放 M 层不合适,V 层更不合适,那就只能放 C 层了。"这应该是大部分遇到胖 C 问题同学的心声吧。
具体怎么瘦身,其实核心思想就是 DRY 和单一职责原则。我们先看 ViewController 应该做的事,总之就是调度 M 和 V 层,管理 ViewContainer,具体如下:
- 管理 View Container 的生命周期
- 负责生成所有的 View 实例,并放入 View Container
- 监听来自 View 与业务有关的事件,通过与 Model 的合作,来完成对应事件的业务。
明白了应该要做的事,再遇到一些其他的代码,放在 ViewController 之前是不是就会有意识地去思考一下了呢?Lighter View Controllers这篇文章已经总结的很好了,所以大家可以参照一下,为自己的 ViewController 瘦瘦身。
解答问题三:提问:"过于强调 Model 与 View 的隔离,会让 View 层的封装性被破坏"
其实不必完全按照 Cocoa 的设计样板来看待这件事情。其实按照 MVC 的原始概念,View 就是附着于 Model 上的一个产物,作用就是将 Model 内容展示出来。微软的 ASP .NET MVC 对 MVC 的理解就是基于这个概念。
所以不必拘泥于必须 V、M 完全分离,MVVM 中的 V 也是对 VM 有一个强持有的。