MVC
MVC(Model-View-Controller)是最老牌的的思想,老牌到4人帮的书里把它归成了一种模式,其中Model就是作为数据管理者,View作为数据展示者,Controller作为数据加工者,Model和View又都是由Controller来根据业务需求调配,所以Controller还负担了一个数据流调配的功能。
在iOS开发领域,我们应当如何进行MVC的划分?
这里面其实有两个问题
1、为什么我们会纠结于iOS开发领域中MVC的划分问题?
2、在iOS开发领域中,怎样才算是划分的正确姿势?
为什么我们会纠结于iOS开发领域中MVC的划分问题?
关于这个,每个人纠结的点可能不太一样。但请允许我猜一下:是不是因为UIViewController中自带了一个View,且控制了View的整个生命周期(viewDidLoad,viewWillAppear...),而在常识中我们都知道Controller不应该和View有如此紧密的联系,所以才导致大家对划分产生困惑?,下面我会针对这个猜测来给出我的意见。
在服务端开发领域,Controller和View的交互方式一般都是这样,比如Yii:
/*
...
数据库取数据
...
处理数据
...
*/
// 此处$this就是Controller
$this->render("plan",array(
'planList' => $planList,
'plan_id' => $_GET['id'],
));
这里Controller和View之间区分得非常明显,Controller做完自己的事情之后,就把所有关于View的工作交给了页面渲染引擎去做,Controller不会去做任何关于View的事情,包括生成View,这些都由渲染引擎代劳了。这是一个区别,但其实服务端View的概念和Native应用View的概念,真正的区别在于:从概念上严格划分的话,服务端其实根本没有View,拜HTTP协议所赐,我们平时所讨论的View只是用于描述View的字符串(更实质的应该称之为数据),真正的View是浏览器。。
所以服务端只管生成对View的描述,至于对View的长相,UI事件监听和处理,都是浏览器负责生成和维护的。但是在Native这边来看,原本属于浏览器的任务也逃不掉要自己做。那么这件事情由谁来做最合适?苹果给出的答案是:UIViewController。
鉴于苹果在这一层做了很多艰苦卓绝的努力,让iOS工程师们不必亲自去实现这些内容。而且,它把所有的功能都放在了UIView上,并且把UIView做成不光可以展示UI,还可以作为容器的一个对象。
看到这儿你明白了吗?UIView的另一个身份其实是容器!UIViewController中自带的那个view,它的主要任务就是作为一个容器。如果它所有的相关命名都改成ViewContainer,那么代码就会变成这样:
- (void)viewContainerDidLoad
{
[self.viewContainer addSubview:self.label];
[self.viewContainer addSubview:self.tableView];
[self.viewContainer addSubview:self.button];
[self.viewContainer addSubview:self.textField];
}
... ...
仅仅改了个名字,现在是不是感觉清晰了很多?如果再要说详细一点,我们平常所认为的服务端MVC是这样划分的:
---------------------------
| C |
| Controller |
| |
---------------------------
/ \
/ \
/ \
------------ ---------------------
| M | | V |
| Model | | Render Engine |
| | | + |
------------ | HTML Files |
---------------------
但事实上,整套流程的MVC划分是这样:
---------------------------
| C |
| Controller |
| \ |
| Render Engine |
| + |
| HTML Files |
---------------------------
/ \
/ \ HTML String
/ \
------------ ---------------
| M | | V |
| Model | | Browser |
| | | |
------------ ---------------
由图中可以看出,我们服务端开发在这个概念下,其实只涉及M和C的开发工作,浏览器作为View的容器,负责View的展示和事件的监听。那么对应到iOS客户端的MVC划分上面来,就是这样:
----------------------------
| C |
| Controller |
| \ |
| View Container |
----------------------------
/ \
/ \
/ \
------------ ----------------------
| M | | V |
| Model | | UITableView |
| | | YourCustomView |
------------ | ... |
----------------------
唯一区别在于,View的容器在服务端,是由Browser负责,在整个网站的流程中,这个容器放在Browser是非常合理的。在iOS客户端,View的容器是由UIViewController中的view负责,我也觉得苹果做的这个选择是非常正确明智的。
因为浏览器和服务端之间的关系非常松散,而且他们分属于两个不同阵营,服务端将对View的描述生成之后,交给浏览器去负责展示,然而一旦view上有什么事件产生,基本上是很少传递到服务器(也就是所谓的Controller)的(要传也可以:AJAX),都是在浏览器这边把事情都做掉,所以在这种情况下,View容器就适合放在浏览器(V)这边。
但是在iOS开发领域,虽然也有让View去监听事件的做法,但这种做法非常少,都是把事件回传给Controller,然后Controller再另行调度。所以这时候,View的容器放在Controller就非常合适。Controller可以因为不同事件的产生去很方便地更改容器内容,比如加载失败时,把容器内容换成失败页面的View,无网络时,把容器页面换成无网络的View等等。
在iOS开发领域中,怎样才算是MVC划分的正确姿势?
这个问题其实在上面已经解答掉一部分了,那么这个问题的答案就当是对上面问题的一个总结吧。
M应该做的事:
给ViewController提供数据
给ViewController存储数据提供接口
提供经过抽象的业务基本组件,供Controller调度
C应该做的事:
管理View Container的生命周期
负责生成所有的View实例,并放入View Container
监听来自View与业务有关的事件,通过与Model的合作,来完成对应事件的业务。
V应该做的事:
响应与业务无关的事件,并因此引发动画效果,点击反馈(如果合适的话,尽量还是放在View去做)等。
界面元素表达
MVCS
苹果自身就采用的是这种架构思路,从名字也能看出,也是基于MVC衍生出来的一套架构。从概念上来说,它拆分的部分
是Model部分,拆出来一个Store。这个Store专门负责数据存取。但从实际操作的角度上讲,它拆开的是Controller。
这算是瘦Model的一种方案,瘦Model只是专门用于表达数据,然后存储、数据处理都交给外面的来做。MVCS使用的前
提是,它假设了你是瘦Model,同时数据的存储和处理都在Controller去做。所以对应到MVCS,它在一开始就是拆分
的Controller。因为Controller做了数据存储的事情,就会变得非常庞大,那么就把Controller专门负责存取数
据的那部分抽离出来,交给另一个对象去做,这个对象就是Store。这么调整之后,整个结构也就变成了真正意义上的
MVCS。
关于胖Model和瘦Model
许多人看见这个是一脸懵逼,根本没听过啊!!!!!!!
直到现在,知道胖Model和瘦Model的概念的人不是很多。大约两三年前国外业界曾经对此有过非常激烈的讨论,
主题就是Fat model, skinny controller。现在关于这方面的讨论已经不多了,然而直到今天胖Model和
瘦Model哪个更好,业界也还没有定论,所以这算是目前业界悬而未解的一个争议。我很少看到国内有讨论这个的资料,
所以在这里我打算补充一下什么叫胖Model什么叫瘦Model。以及他们的争论来源于何处。
什么叫胖Model?
胖Model包含了部分弱业务逻辑。胖Model要达到的目的是,Controller从胖Model这里拿到数据之后,不
用额外做操作或者只要做非常少的操作,就能够将数据直接应用在View上。举个例子:
Raw Data:
timestamp:1234567
FatModel:
@property (nonatomic, assign) CGFloat timestamp;
- (NSString *)ymdDateString; // 2015-04-20 15:16
- (NSString *)gapString; // 3分钟前、1小时前、一天前、2015-3-13 12:34
Controller:
self.dateLabel.text = [FatModel ymdDateString];
self.gapLabel.text = [FatModel gapString];
把timestamp转换成具体业务上所需要的字符串,这属于业务代码,算是弱业务。FatModel做了这些弱业务之后,
Controller就能变得非常skinny,Controller只需要关注强业务代码就行了。众所周知,强业务变动的可能性要
比弱业务大得多,弱业务相对稳定,所以弱业务塞进Model里面是没问题的。另一方面,弱业务重复出现的频率要大于
强业务,对复用性的要求更高,如果这部分业务写在Controller,类似的代码会洒得到处都是,一旦弱业务有修改
(弱业务修改频率低不代表就没有修改),这个事情就是一个灾难。如果塞到Model里面去,改一处很多地方就能跟着
改,就能避免这场灾难。
然而其缺点就在于,胖Model相对比较难移植,虽然只是包含弱业务,但好歹也是业务,迁移的时候很容易拔出萝卜
带出泥。另外一点,MVC的架构思想更加倾向于Model是一个Layer,而不是一个Object,不应该把一个Layer应该做
的事情交给一个Object去做。最后一点,软件是会成长的,FatModel很有可能随着软件的成长越来越Fat,最终难以
维护。
什么叫瘦Model?
瘦Model只负责业务数据的表达,所有业务无论强弱一律扔到Controller。瘦Model要达到的目的是,尽一切可能
去编写细粒度Model,然后配套各种helper类或方法来对弱业务做抽象,强业务依旧交给Controller。举个例子:
Raw Data:
{
"name":"casa",
"sex":"male",
}
SlimModel:
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *sex;
Helper:
#define Male 1;
#define Female 0;
+ (BOOL)sexWithString:(NSString *)sex;
Controller:
if ([Helper sexWithString:SlimModel.sex] == Male) {
...
}
由于SlimModel跟业务完全无关,它的数据可以交给任何一个能处理它数据的Helper或其他的对象,来完成业务。
在代码迁移的时候独立性很强,很少会出现拔出萝卜带出泥的情况。另外,由于SlimModel只是数据表达,对它进行维
护基本上是0成本,软件膨胀得再厉害,SlimModel也不会大到哪儿去。
缺点就在于,Helper这种做法也不见得很好,这里有一篇[文章](http://nicksda.apotomo.de/2011/10/rails-misapprehensions-helpers-are-shit/)
批判了这个事情。另外,由于Model的操作会出现在各种地方,SlimModel在一定程度上违背了DRY
(Don't Repeat Yourself)的思路,Controller仍然不可避免在一定程度上出现代码膨胀。
话说回来,MVCS是基于瘦Model的一种架构思路,把原本Model要做的很多事情中的其中一部分关于数据存储的代码
抽象成了Store,在一定程度上降低了Controller的压力。