MV(X)系列概要
做iOS开发也有一段时间了,最近闲暇之余总结了一下几个iOS框架,每个人对架构和设计模式都有不同的理解,在此记录下我的一些小见解,仅供参考,欢迎批评指正。
当今我们已经有很架构设计模式方面的选择:
A.MVC
B.MVP
C.MVVM
D.VIPER
前三种设计模式都把一个应用中的实体分为以下三类:
A: Models–负责主要的数据或者操作数据的数据访问层,可以想象 Perspn 和 PersonDataProvider 类。
B: Views–负责展示层(GUI),对于iOS环境可以联想一下以 UI 开头的所有类。
C: Controller/Presenter/ViewModel–负责协调 Model 和 View,通常根据用户在View上的动作在Model上作出对应的更改,同时将更改的信息返回到View上.
MVC的过去
在我们探讨Apple的MVC模式之前,我们来看下传统的MVC模式。
在这里,View并没有任何界限,仅仅是简单的在Controller中呈现出Model的变化。想象一下,就像网页一样,在点击了跳转到某个其他页面的连接之后就会完全的重新加载页面。尽管在iOS平台上实现这这种MVC模式是没有任何难度的,但是它并不会为我们解决架构问题带来任何裨益。因为它本身也是,三个实体间相互都有通信,而且是紧密耦合的。这很显然会大大降低了三者的复用性,而这正是我们不愿意看到的。
苹果推荐的MVC–愿景
控制器来作为视图和模型中间的中转站,这样视图和模型之间互相就没有直接联系了。这样的话,控制器的可复用性就变得最低,但是这个对于我们来说也是可接受的,因为我们需要有一个地方来放那些不方便放在模型中的复杂业务逻辑。
理论上来讲,这种结构看起来非常直接,但是是不是觉得有点不对劲?甚至听到过有人叫 MVC 为重控制器模式。此外,对于 iOS 开发者来说,给控制器减轻负担已经成为一个重要的话题。为什么苹果会采用仅仅改进过一点点的传统 MVC 模式呢?
现实(Reality)
Cocoa MVC 鼓励你写重控制器是因为它们互相之间在视图的生命周期中互相牵扯,以至于很难将它们分开。虽然你也可能有一些办法把一些业务逻辑和数据转模型的工作放到 Model 中来,但是对于把负担分摊到 View 上却没有什么办法,大多数情况中,View 的所有功能就是给 Controller 发送动作(action),而 Controller 则会成为所有东西的代理或者数据源,并且通常会发送或者取消网络请求等等等等,你可以尽情想象。
评估:
(1) 划分(Distribution)–View 和 Model 确实是分开了,但是View 和 Controller 紧紧的联系在一起
(2)可测试性–由于功能划分的不好,你可能只能测试你的 Model
(3)易用性–与其他模式相比代码量最小,另外,每个人都对他很熟悉,即使是一个不是非常有经验的开发者也能进行维护
就开发速度而言,Cocoa MVC 是最好的架构模式。
MVP
Cocoa MVC’s promises delivered
它看上去是不是非常像 Apple’s MVC ?是的,确实很像,并且叫做MVP(Passive View variant)。但是等一下,这意味着 Apple’s MVC 实质上就是 MVP 吗?不是的,还记得 View 是紧紧和 Controller 联系在一起的吧,而在 MVP 中,作为中转站的 Presenter 与视图控制器的生命周期没有任何关联,并且 View 很容易被模拟,所以在 Presenter 中没有任何页面布局的代码,但是 Presenter 有责任通过数据和状态来更新 View。
MVP 的特点:
(1) 划分(distribution)–大部分的任务都被划分到 Presenter 和 Model 中,而 View不太灵活(例子中的 Model 也不太灵活)
(2) 可测试性–非常出色,我们可以通过 View 来测试大部分的业务逻辑
(3)易用性–在我们那个不切实际的小例子里,MVP 的理念是非常清晰的,但是代码量是 MVC 模式的两倍
MVP 在 iOS 中使用意味着非常好的可测试和非常多的代码
MVVM
概述
引用自iOS应用架构谈:
http://www.cocoachina.com/ios/20150525/11919.html
MVVM的出现主要是为了解决在开发过程中Controller越来越庞大的问题,变得难以维护,所以MVVM把数据加工的任务从Controller中解放了出来,使得Controller只需要专注于数据调配的工作,ViewModel则去负责数据加工并通过通知机制让View响应ViewModel的改变。
MVVM是基于胖Model的架构思路建立的,然后在胖Model中拆出两部分:Model和ViewModel。ViewModel本质上算是Model层(因为是胖Model里面分出来的一部分),所以View并不适合直接持有ViewModel,因为ViewModel有可能并不是只服务于特定的一个View,使用更加松散的绑定关系能够降低ViewModel和View之间的耦合度。
还有一个让人很容易忽略的问题,大部分国内外资料阐述MVVM的时候都是这样排布的:
View<->ViewModel <->Model
造成了MVVM不需要Controller的错觉,现在似乎发展成业界开始出现MVVM是不需要Controller的声音了。其实MVVM是一定需要Controller的参与的,虽然MVVM在一定程度上弱化了Controller的存在感,并且给Controller做了减负瘦身(这也是MVVM的主要目的)。但是,这并不代表MVVM中不需要Controller,MMVC和MVVM他们之间的关系应该是这样:
View <-> C <-> ViewModel <->Model
所以使用MVVM之后,就不需要Controller的说法是不正确的。严格来说MVVM其实是MVCVM。从中可以得知,Controller夹在View和ViewModel之间做的其中一个主要事情就是将View和ViewModel进行绑定。在逻辑上,Controller知道应当展示哪个View,Controller也知道应当使用哪个ViewModel,然而View和ViewModel它们之间是互相不知道的,所以Controller就负责控制他们的绑定关系,所以叫Controller/控制器就是这个原因。
前面讲了那么多,其实归根结底就是一句话:在MVC的基础上,把C拆出一个ViewModel专门负责数据处理的事情,就是MVVM。然后,为了让View和ViewModel之间能够有比较松散的绑定关系,于是我们使用ReactiveCocoa,KVO,Notification,block,delegate和target-action都可以用来做数据通信,从而来实现绑定,但都不如ReactiveCocoa提供的RACSignal来的优雅,如果不用ReactiveCocoa,绑定关系可能就做不到那么松散那么好,但并不影响它还是MVVM。
MVVM(View-ViewManger-C-ViewModel-Model)
View - 用来呈现用户界面
ViewManger - 用来处理View的常规事件,负责管理View
Controller - 负责ViewManger和ViewModel之间的绑定,负责控制器本身的生命周期。
ViewModel - 存放各种业务逻辑和网络请求
Model - 用来呈现数据
这种设计的目的是保持View和Model的高度纯洁,提高可扩展性和复用度。在日常开发中,ViewModel是为了拆分Controller业务逻辑而存在的,所以ViewModel需要提供公共的服务接口,以便为Controller提供数据。而ViewManger的作用相当于一个小管家,帮助Controller来分别管理每个subView,ViewManger负责接管来自View的事件,也负责接收来自Controller的模型数据,而View进行自己所负责的视图数据绑定工作。Controller则是最后的大家长,负责将ViewModel和ViewManger进行绑定,进行数据转发工作。把合适的数据模型分发给合适的视图管理者。
日常开发中,往往一个视图页面交由一个控制器进行管理,而一个页面上又有N个小的子页面,这就要求我们来对这些视图进行合适的分层处理,拆分视图,将这些视图进行封装,将复杂View抽象成独立的类,不必暴露出具体的实现细节。这样做的好处是,简化应用层的层级复杂度,同时也方便进行管理,视图结构就会变得很清晰。子视图具体的内部事件,可通过代理模式或者Block交由ViewManger处理,因为视图是可以复用的,而其中的事件响应代码往往是根据不同的业务是有差异的。所以可能会有下面两种情况出现:
- View很纯洁,需要复用View,若业务逻辑变化则切换ViewManger。
- ViewManger也比较纯洁,若业务逻辑不变,而View需要大变,则切换View即可,保证View中的protocol或者block一致即可<最好是通过协议提前规范好>。
这样就实现了互相的封装,两者之间只通过protocol或者block进行交流通信,降低了代码的耦合度。尽量使用protocol和category来制定对象之间的通信规范,来降低代码的侵入性。
这样的架构设计,就像一条生产线,ViewModel进行数据的采集和加工,Controller则进行数据的装配和转发工作,ViewManger进行接收转发分配来的数据,从而进行负责View的展示工作和管理View的事件。这样,不管哪个环节,都是可以更换的,同时也提高了复用性。
架构讲解
以上图做为讲解demo,最然很简单,但是也能够很好的阐述了,理解思想才是最重要的。
首先我们来拆分这个页面,第一个为控制器。暂且命名为MyController,上面有两个直接子视图,按钮MyBtn和页面比较复杂的子视图MyView,MyView中有MyViewBtn1和MyViewBtn2还有一个MyViewLabel视图。
具体结构如下:
MyController
MyBtn
MyView
MyViewBtn1
MyViewBtn2
MyViewLabel
界面分析完了,现在可以进行代码的架构工作了。
首先需要建立一个ViewModel,使它能够源源不断的进行数据的生产,并提供数据给MyController;然后建立一个ViewManger负责管理MyView,当然,Model模型数据必不可少。这些工作完成之后,代码结构变为:
Controller - - 存放MyController
ViewModel - - 存放MyViewModel
View - - 存放MyView
ViewManger - - 存放MyViewManger
Model - - 存放MyModel
控制器中的代码结构如下图:
当用户点击MyBtn按钮触发动作时,控制器就会就将ViewMode中加载的数据模型转发分配给ViewManger中的回调函数- (void)smk_viewMangerWithModel:(NSDictionary * (^) ( ))dictBlock接收。
// 两种消息传递方式,开发时任选其一即可
- (void)smk_viewMangerWithSubView:(UIView *)subView {
__weak typeof(self.thirdView) weakThirdView = self.thirdView;
__weak typeof(self) weakSelf = self;
// btnClickBlock
weakThirdView.btnClickBlock = ^() {
[weakSelf smk_viewMangerWithHandleOfSubView:weakThirdView info:@"click"];
};
}
// 两种消息传递方式,开发时任选其一即可
- (void)smk_view:(__kindof UIView *)view withEvents:(NSDictionary *)events {
NSLog(@"----------%@", events);
if ([[events allKeys] containsObject:@"jump"]) {
FirstVC *firstVC = [UIViewController svv_viewControllerWithStoryBoardName:@"Main" identifier:@"FirstVCID"];
[view.sui_currentVC.navigationController pushViewController:firstVC animated:YES];
}
}
其中,MyViewModel中的加载代码如下,如上所述,它的工作就是分解以前控制器做的一些事情。
- (void)smk_viewModelWithGetDataSuccessHandler:(void (^)())successHandler {
// 博客中省略,查看详细请参考demo
}
- (instancetype)getRandomData {
if (self.smk_dataArrayList.count > 0) {
u_int32_t index = arc4random_uniform((u_int32_t)self.smk_dataArrayList.count);
return self.smk_dataArrayList[index];
}
return nil;
}
MyViewManger中的代码如下,它实现了MVVMViewMangerProtocol协议的三个方法:
// 此方法用来接收处理来自所管理View的一些事件。
- (void)smk_viewMangerWithSubView:(UIView *)subView;
// 此方法将view的父视图传递过来,用来布局当前View
- (void)smk_viewMangerWithSuperView:(UIView *)superView;
// 根据所传入的view和info信息分别实现具体的方法
- (void)smk_viewMangerWithHandleOfSubView:(UIView *)subView info:(NSString *)info;
// 两种消息传递方式,开发时任选其一即可
- (void)smk_viewMangerWithSubView:(UIView *)subView {
__weak typeof(self.thirdView) weakThirdView = self.thirdView;
__weak typeof(self) weakSelf = self;
// btnClickBlock
weakThirdView.btnClickBlock = ^() {
[weakSelf smk_viewMangerWithHandleOfSubView:weakThirdView info:@"click"];
};
}
// 两种消息传递方式,开发时任选其一即可 ---> 视图delegate
- (void)smk_view:(__kindof UIView *)view withEvents:(NSDictionary *)events {
NSLog(@"----------%@", events);
if ([[events allKeys] containsObject:@"jump"]) {
FirstVC *firstVC = [UIViewController svv_viewControllerWithStoryBoardName:@"Main" identifier:@"FirstVCID"];
[view.sui_currentVC.navigationController pushViewController:firstVC animated:YES];
}
}
- (void)smk_viewMangerWithSuperView:(UIView *)superView {
self.thirdView.frame = CGRectMake(0, 66, [UIScreen mainScreen].bounds.size.width, 200);
[superView addSubview:self.thirdView];
}
- (void)smk_viewMangerWithHandleOfSubView:(UIView *)view info:(NSString *)info {
if ([info isEqualToString:@"click"]) {
[view configureViewWithCustomObj:self.smk_model];
}
}
MyView中的代码如下,主要是负责管理自身的内部控件视图,并根据业务逻辑需要定义了一些基本事件,通过交给ViewManger来实现:
- (IBAction)testBtnClick:(UIButton *)sender {
if (self.btnClickBlock) {
self.btnClickBlock();
}
}
- (IBAction)jumpOtherVC:(UIButton *)sender {
if (self.delegate && [self.delegate respondsToSelector:@selector(smk_view:withEvents:)]) {
[self.delegate smk_view:self withEvents:@{@"jump": @"vc"}];
}
}
// 根据传入的model配置需要显示的内容
- (void)configureViewWithCustomObj:(id)obj {
if (!obj) return;
ThirdModel *thirdModel = (ThirdModel *)obj;
self.testLabel.text = thirdModel.title;
}
这样把各个部分区分开来,是不是感觉代码结构十分清晰了呢,当然可以根据个人习惯来进行修改,代码实现因人而异,但是思想确是互通的。把合适的业务逻辑交给最合适的对象去处理实现,只需要遵守这么一个基本原则就可以了。
至于是否采用更轻量级的ViewController做法,即 通过将各个 protocol 的实现挪到 ViewController 之外,来为 ViewController 瘦身 ,众口不一。以UITableView为例,我的做法是:
如果只是在页面上进行简单的展示,并不设计负责的业务逻辑时,会将UITableViewDelegate与UITableViewDataSource单独放到一个Handler钟进行处理,抽象出tableHander,由MVVMTableDataDelegate进行负责管理;
当然,事实上,实际开发中,每个tableView页面都很复杂,有很多逻辑要处理,这时候只能考虑将protocol重新请回Controller中了,因为View层与ViewController层本身是持有与被持有的依赖关系,所以任何类作为ViewController的类内实例来实现协议回调,实际上都是在跨层调用,所以,随着时间和业务逻辑的愈来愈复杂,就注定要以额外的接口为代价,换言之,ViewController 的内聚性变差了。
总之,具体情况具体分析,采用最合适的方式来处理应对不同的问题。兵来将挡,水来土掩。本文的相关Demo见github,实现的功能并不复杂,仅供参考,欢迎补充。
项目传送门:https://github.com/lovemo/MVVMFramework
VIPER
从乐高玩具的搭建经验转换到 iOS app 的设计
VIPER是我们最后一个要介绍的架构,它不是MV(X)系列的架构。
到现在为止,我们应该都觉得职责划分的颗粒度还是不错的。在 VIPER 中对于职责的划分提出了另一种方式,这次我们有五层:
- Interactor–包括和数据相关的业务逻辑(Entities)或者网络请求,比如创建entities 类的对象或者把它们从服务器中抓取出来。为了达到这些目的你会用到通常会被看做外部依赖而不被看做 VIPER 单元的一些服务(Services)和管理者(Managers)
- Presenter–包括 UI 相关(UIKit 之外)的一些业务逻辑,调用 Interactor 中的一些方法
- Entities–纯粹的数据对象,并非是数据访问层,数据访问是 Interactor 层的任务
- Router–负责 VIPER 模块之间的切换
它的特性:
(1)划分(distribution)–毫无疑问,在层次职责划分方面,VIPER 是最棒的
(2)可测试性–理所当然的,非常好的层次划分带来好的可测试性
(3)易用性–想你想的那样,上述两个方面都牺牲(系统)可维护性的,你需要编写许多的仅有少量功能的接口
参考链接:
http://www.cocoachina.com/ios/20160108/14916.html
http://www.jianshu.com/p/87ac2f075a5b
http://www.jianshu.com/p/5161acddca53