为什么要用 MVVM 代替 MVC?#

image)Apple 倡导开发者们使用 MVC 模式开发 App 程序,但很多人都没有严格按照 MVC 的模式去开发,只是让程序的架构看上去像 MVC,而实际上是 MC 或 VC,很多入门开发者都有一个通病,就是把所有的逻辑,界面生成都写进 ViewController 中,这样 ViewController 就变成了一个 Massive View Controller(重量级视图控制器)。重量级视图控制器会让整个 ViewController 变得非常复杂且不可维护,让维护者崩溃却无从下手,只能忍痛默默的重写整个逻辑。这是传统 MVC 模式:

有一种解决方案,可以解决 Massive View Controller 的问题,那就是 MVVM。

常用的情况是将所有业务不加区分都放进 ViewController 中这样会不可避免的让 ViewController 变得臃肿,这是造成重量级视图控制器的重要原因。一个 ViewController 中包含大量业务的细节,将使这个 ViewController 在业务协调和调用中迷失,将让这个业务变得非常混乱,让业务逻辑变得无法维护。由于重量级 ViewController 的复杂性,其代码也将将难以复用。以上原因是传统 MVC 难以解决的,为解决这些问题,要采用 MVVM 的开发模式。
MVVM 简单说就是将部分逻辑从 ViewController 中拆分出来,并整合起来在 ViewController 和 Model 中间加多一个 ViewModel,ViewModel 不直接引用 View,ViewController 也不引用 Model 中的方法,所有网络回调数据处理等逻辑都放到 ViewModel 中,ViewController 通过 ViewModel 来请求数据和更新数据。
如何实践 MVVM#
第一步:创建 Model#
AFNetworking 请求方法放到 Model 中
@interface Model : NSObject
@property (nonatomic, copy) NSString *col;
@property (nonatomic, copy) NSString *sort;
@property (nonatomic, copy) NSString *tag3;
@property (nonatomic, assign) NSInteger startIndex;
@property (nonatomic, assign) NSInteger returnNumber;
@property (nonatomic, strong) NSArray *imgs;
@property (nonatomic, copy) NSString *tag;
@property (nonatomic, assign) NSInteger totalNum;
+ (void)getImagesListWithPage: (NSInteger)aPage SuccessBlock :(SuccessBlock)success FailBlock :(FailBlock)fail;具体实现,不多说:
@implementation Model
+ (void)getImagesListWithPage: (NSInteger)aPage SuccessBlock :(SuccessBlock)success FailBlock :(FailBlock)fail {
NSString *urlString = [NSString stringWithFormat:@"%@%ld%@",
@"http://image.baidu.com/data/imgs?col=%e7%be%8e%e5%a5%b3&tag=%e5%b0%8f%e6%b8%85%e6%96%b0&sort=0&pn=1",
aPage,@"&rn=1&p=channel&from=1"];
AFHTTPRequestOperationManager *managere = [AFHTTPRequestOperationManager manager];
[managere GET:urlString parameters:nil
success:^(AFHTTPRequestOperation * _Nonnull operation, id _Nonnull responseObject) {
success(responseObject,nil);
NSLog(@"success");
} failure:^(AFHTTPRequestOperation * _Nullable operation, NSError * _Nonnull error) {
fail(nil,error);
NSLog(@"fail");
}];
}
@end第二步:创建 ViewModel#
- ViewModel 的属性
- data : 请求获取的数据
- racMsg : 请求成功和失败的信号量(主要用 KVO 对这个进行监视)
@interface ViewModel : NSObject
@property (strong,nonatomic) NSDictionary *data;
@property (strong,nonatomic) NSString *racMsg;
- (void)getImagesList;
- (void)getNextImagesList;
- (void)getPreImagesList;
@endViewController 中主要监视 ViewModel 的 racMsg 来发现 data 更新
#define WS(weakSelf) __weak __typeof(&*self)weakSelf = self;
@interface ViewModel()
@property (nonatomic) NSInteger currentPage;
@end
@implementation ViewModel
- (instancetype)init {
self = [super init];
self.currentPage = 0;
return self;
}
- (void)getImagesList {
WS(ws)
[Model getImagesListWithPage:0
SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.data = responseObjectDict;
ws.racMsg = @"success";
} FailBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.data = nil;
ws.racMsg = @"fail";
}];
}
- (void)getNextImagesList {
WS(ws)
self.currentPage++;
[Model getImagesListWithPage:self.currentPage
SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.data = responseObjectDict;
ws.racMsg = @"success";
} FailBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.data = nil;
ws.racMsg = @"fail";
}];
}
- (void)getPreImagesList {
WS(ws)
self.currentPage = self.currentPage == 0 ? 0 : self.currentPage-1;
[Model getImagesListWithPage:self.currentPage
SuccessBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.data = responseObjectDict;
ws.racMsg = @"success";
} FailBlock:^(NSDictionary *responseObjectDict, NSError *error) {
ws.data = nil;
ws.racMsg = @"fail";
}];
}
@end可能会有人觉得为什么不直接监视 data,但我个人更倾向于采用一种类似于信号量的机制,监听特定的信号来更新数据。如果直接监视 data,则 data 只有 nil 和非 nil 两种情况,要进一步区分请求的状态的话必须要对 data 进行解析,增加了转换成本,不如直接采用多一个属性变量进行判断和协调
第三步:ViewController 中 KVO 设置#
ViewController 直接持有 viewModel
@interface ViewController ()
@property (strong,nonatomic) ViewModel *viewModel;
@property (strong,nonatomic) UITextView *showTextView;
@end加载 ViewController 时初始化 KVO 和调用 ViewModel 方法 getImagesList 来请求数据
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
// requestData
[self _initViews];
[self setupKVO];
[self.viewModel getImagesList];
}ViewController 销毁时去除 KVO
- (void)dealloc {
[self removeKVO];
}KVO 相关的函数。observeValueForKeyPath 只需对 racMsg 进行判断就可以知道 data 的值是否更新了,如果更新了就更新一下 View。
pragma mark - KVO
- (void)setupKVO {
[self.viewModel addObserver:self
forKeyPath:@"racMsg" options:(NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld) context:nil];
}
- (void)removeKVO {
[self.viewModel removeObserver:self forKeyPath:@"racMsg"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"racMsg"]) {
if ([_viewModel.racMsg isEqualToString:@"success"]) {
_showTextView.text = [NSString stringWithFormat:@"%@",_viewModel.data];
}
else {
_showTextView.text = @"error";
}
}
}按钮点击事件和 View 的初始化
#pragma mark - Event Response
- (void)getPre {
[self.viewModel getPreImagesList];
}
- (void)getNext {
[self.viewModel getNextImagesList];
}
#pragma mark - Private
- (void)_initViews {
UIButton *preBtn = [[UIButton alloc]initWithFrame:CGRectMake(20, 50, 200, 40)];
[preBtn setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
[preBtn setTitle:@"Pre" forState:UIControlStateNormal];
[preBtn addTarget:self action:@selector(getPre) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:preBtn];
UIButton *nextBtn = [[UIButton alloc]initWithFrame:CGRectMake(20, 150, 200, 40)];
[nextBtn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
[nextBtn setTitle:@"nextBtn" forState:UIControlStateNormal];
[nextBtn addTarget:self action:@selector(getNext) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:nextBtn];
_showTextView = [[UITextView alloc]initWithFrame:CGRectMake(0, 200, 320, 200)];
_showTextView.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:_showTextView];
}实践 MVVM 的具体好处的例子#
例子 1#
ViewController 需要一个额外请求一个文章列表,这个文章列表的请求参数与当前请求图片列表的接口返回结果没有任何关联,可以并行请求。 在这种情况下,在 ViewModel 中加入方法 getArticleList()和属性 articleList 以及 articleMsg,然后在 ViewController 中需要调用该方法的位置调用该方法即可。KVO 中的 observeValueForKeyPath 方法稍微修改一下
(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"racMsg"]) {
if ([_viewModel.racMsg isEqualToString:@"success"]) {
_showTextView.text = [NSString stringWithFormat:@"%@",_viewModel.data];
}
else {
_showTextView.text = @"error";
}
}
else if([keyPath isEqualToString:@"articleMsg"]) {
if ([_viewModel.articleMsg isEqualToString:@"success"]) {
_articleTextView.text = _viewModel.articleList
}
else {
_articleTextView.text = @"error";
}
}
}可见并行业务功能上的扩展是非常简单的,整个 Controller 的总体逻辑几乎不用怎么变化,只需要改动局部细节即可
例子 2#
同例子 1,但是文章列表的请求参数需要通过图片列表接口返回的结果获取,请求是串联嵌套的(即先请求图片列表接口,请求完成后,根据返回结果再请求文章列表接口) 对于嵌套的请求,如果采用传统 MVC 模式,就要在 ViewController 中加入两个 block,一个嵌套另外一个,这样会让代码变得非常难看,而且会让子 block 依赖于父 block,难以对其进行拆分。但如果采用 MVVM,则会将所有的请求变化都置于 KVO 的监控之下,并作出统一的处理。 ViewModel 中的与例子 1 一样,但 ViewController 中的处理稍微不同
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:@"racMsg"]) {
if ([_viewModel.racMsg isEqualToString:@"success"]) {
_showTextView.text = [NSString stringWithFormat:@"%@",_viewModel.data];
// 请求文章
[_viewModel getArticleList];
}
else {
_showTextView.text = @"error";
}
}
else if([keyPath isEqualToString:@"articleMsg"]) {
if ([_viewModel.articleMsg isEqualToString:@"success"]) {
_articleTextView.text = _viewModel.articleList
}
else {
_articleTextView.text = @"error";
}
}
}可以看出还是不需要改动大逻辑,即可对有依赖的业务进行扩展。
例子 3#
ViewController中有很多数据转换逻辑,多个ViewController的数据转换逻辑都相同的情况。
ViewModel 中不仅只包含请求逻辑,还可以包含数据转换的逻辑,还有一些不知要怎么归类的杂七杂八的逻辑。多个 ViewController 发生相同的数据转换的情况是经常会有的,如果把数据转换逻辑写到 Controller 之中,会让每一个 Controller 都持有一个转换逻辑,这对于数据转换逻辑的统一来说是非常糟糕的。如果把相同的数据转换逻辑都抽象封装到同一个 ViewModel 中,ViewController 不直接持有数据转换逻辑,而是通过 ViewModel 来调用的话,每个 ViewController 只需要维护一个 ViewModel 实例即可,所有转换细节都可以在 ViewModel 中进行统一修改。Controller 只关心数据和数据与 View 的交互,不应该关心数据之间的转换和数据怎样获取的,这应该是 MVVM 的一个原则。
总结#
- MVVM 的核心在于绑定,本文采用的是 KVO 的绑定机制,能够很好与 Objective-C 和 Cocoa 结合起来,不需要借用第三方的类库进行数据绑定。 除了使用 KVO,业界通常采用的是 ReactiveCocoa。但是,ReactiveCocoa 的学习成本过高,不适合轻量级的开发,而 MVVM 只是一种开发模式,并不是一种具体的框架,所以如果不是非常想深入使用 MVVM 的精髓的话,是没有必要去学习 ReactiveCocoa 的。网上还有一些讨论 MVVM 的博客提到 ViewModel 直接对 View 进行操作,其实这是一种很不严谨的做法,MVVM 中的 ViewModel 不应该关心 View 的显示,只应该关心数据的获取和转换,View 如何显示那是 ViewController 的职责。所以凡是在 ViewModel 中引用了 UIKit 的,个人认为都不是一种严格意义上的 MVVM。 当然 MVVM 也有其自身的不足,比如引入 ViewModel 之后,文件数量增加了不少,总的代码量其实也会增加,这对于极简主义者来说并不是一种很好的模式。而且 MVVM 的开发思路与 MVC 是不同的,开发者要转换思路采用 MVVM 的开发方式其实还是有不少的学习成本,而且对于大部分简单业务来说,使用 MVVM 会增加业务的复杂度,显得臃肿和多余。本文中使用 KVO 的 MVVM 模式,从本质上来说其实是 MVC 的衍生,把 C 中的一部分拆分出来并隔离 M 和 C,所以从模式上来说,这样是完全可以兼容传统的 MVC 开发模式的。因此,对于简单的业务,可以直接采用 MVC 的模式开发,不需要额外创建一个 ViewModel。对于复杂业务,就采用 KVO 的 MVVM 模式,进行业务拆分和复用。这种折中的方法能够将 MVC 和 MVVM 的优点都利用起来,避免只使用一个造成开发效率上的降低。 MVVM 不应该被误解和神化,使用 MVVM 只是提供多了一个不错的选择,要不要使用它,还是要看具体的项目而定。但是用上了,就停不下来了。
总结#
- 不管 MVVM 还是 MVC 是一种拆分思维,将业务逻辑和显示逻辑进行拆分。减少 controller 的以及 model 层的代码量。利于后期维护。