作为一个iOS程序员,MVC一定是我们耳熟能详的一种架构模式,而且当你的项目规模不大的时候,MVC也确实有它的优势,它的开发效率确实是足够高。但当你的项目发展的一定的规模,你会发现传统的MVC模式会导致C层代码量剧增,维护困难等一系列问题,这个时候我们就需要考虑一些其它模式了。
MV(X)的基本要素
常用的架构模式
- MVC
- MVVM
- MVP
- VIPER
前面三种模式都由三个模块组成:
- Models —— 数据层,负责数据的处理。
- Views —— 展示层,即所有的UI
- Controller/Presenter/ViewModele(控制器/展示器/视图模型)——它们负责View与Mode之间的调配
MVC
传统的MVC
我们所熟知的MVC其实Apple给我们提供的Cocoa MVC,但其实MVC最先产生于Web,它原来的样子应该是这样的
在这种架构下,View是无状态的,在Model变化的时候它只是简单的被Controller重绘,比如网页中你点击了一个新的链接,整个页面就重新加载。尽管这种MVC在iOS应该里面可以实现,但是由于MVC的三个模块都紧密耦合了,每一个模块都和其它两种模块有联系,所以即便是实现了也没有什么意义。这种耦合还降低了它们的可重用性,所以,传统的MVC在iOS中可以舍弃了。
Apple的MVC
Apple提供的MVC中,View和Model之间是相互独立的,它们只通过Controller来相互联系。可惜的是Controller得重用性太差,因为我们一般都把冗杂的业务逻辑放在了Controller中。
现实中,我们的MVC一般是这样的
为什么会这样呢?主要还是因为我们的UIViewController它本身就拥有一个VIew,这个View是所有视图的根视图,而且View的生命周期也都由Controoler负责管理,所以View和Controller是很难做到相互独立的。虽然你可以把控制器里的一些业务逻辑和数据转换工作交给Model,但是你却没有办法将一些工作让View来分摊,因为View的主要职责只是将用户的操作行为交给Controller去处理而已。于是Controller最终就变成了所有东西的代理和数据源,甚至还有网络请求.....还有......所以我们写的Controller代码量一般都是非常大的,随着当业务需求的增加,Controller的代码量会一直增长,而相对来说View和Model的代码量就比较稳定,所以也有人把MVC叫做Massive View Controller,因为Controller确实显得有些臃肿。
在这里关于Model的划分,其实有一个胖Model和瘦Model之分,它们的差别主要就是把Controller的部分数据处理职责交给了胖Model。
胖Model(Fat Model):
胖Model包含了部分弱业务逻辑。胖Model要达到的目的是,Controller从胖Model这里拿到数据之后,不用做额外的操作或者只做非常少的操作就能将数据应用在View上。
FatModel做了这些弱业务之后,Controller可以变得相对skinny一点,它只需要关注强业务代码。而强业务变动的可能性要比弱业务大得多,弱业务相对稳定,所以弱业务塞给Model不会有太大问题。另一方面,弱业务重复出现的频率要大于强业务,对复用性要求更高,如果这部分业务写在Controller,会造成代码冗余,类似的代码会洒得到处都是,而且一旦弱业务有修改,你就会需要修改所有地方。如果塞到了Model中,就只需要改Model就够了。
但是胖Mpdel也不是就是没有缺点的,它的缺点就在于胖Model相对比较难移植,虽然只是包含弱业务,但是它毕竟也是业务,迁移的时候很容易拔出罗布带出泥,也就是说它耦合了它的业务。而且软件是会成长的,FatModel也很有可能随着软件的成长越来越Fat,最后难以维护。
瘦Model(Slim Model):
瘦Model只负责业务数据的表达,所有业务无论强弱一律人给Controller。瘦Model要达到的目的是,尽一切可能去编写细粒度Model,然后配套各种helper类或者方法来对弱业务做抽象,强业务依旧交给Controller。
由于Slim Model跟业务完全无关,它的数据可以交给任何一个能处理它数据的Helper或其他的对象,来完成业务。在代码迁移的时候独立性很强,很少会出现拔出萝卜带出泥的情况。另外,由于SlimModel只是数据表达,对它进行维护基本上是0成本,软件膨胀得再厉害,SlimModel也不会大到哪儿去。缺点就在于,Helper这种做法也不见得很好,由于Model的操作会出现在各种地方,SlimModel很容易出现代码重复,在一定程度上违背了DRY(Don’t Repeat Yourself)的思路,Controller仍然不可避免在一定程度上出现代码膨胀。
综上所述,Cocoa MVC在各方面的表现如下:
- 划分 - View 和 Model 确实是实现了分离,但是 View 和 Controller 耦合的太 厉害
- 可测性 - 因为划分的不够清楚,所以能测的基本就只有 Model 而已
- 易用 - 相较于其他模式,它的代码量最少。而且基本上每个人都很熟悉它,即便是没太多经验的开发者也能维护。
MVP
看起来和Cocoa MVC很像,也确实很像。但是,在MVC中View和COntroller是紧密耦合的,而在MVP中,Presenter完全不关注ViewController的生命周期,而且View也能被简单mock出来,所以在Presenter里面基本没有什么布局相关的代码,它的职责只是通过数据和状态更新View。
而且在MVP中,UIVIewController的那些子类其实是属于View的。这样就提供了更好的可测性,只是开发速度会更高,因为你必须手动去创建数据和绑定事件。
下面我写了个简单的Demo
由于这里主要是学习架构模式思想,所以我的命名简单粗暴,希望大家理解。
界面也很简单,就是通过点击按钮修改两个label显示的内容
Model很简单,就是一个数据结构,但在实际应用中,你可以将网络请求等一些数据处理放在这里
@interface Model : NSObject
@property (nonatomic, strong) NSString *first;
@property (nonatomic, strong) NSString *second;
@end
要让Presenter和View通信,所以我们定义一个协议,以实现Presenter向View发送命令
@protocol MyProtocol <NSObject>
- (void)setFirst:(NSString *)first;
- (void)setSecond:(NSString *)second;
@end
view/VIewController,实现该协议
.h
@interface ViewController : UIViewController
@property (nonatomic, strong) UILabel *firstLabel;
@property (nonatomic, strong) UILabel *secondLabel;
@property (nonatomic, strong) UIButton *tapButton;
@end
.m主要代码
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.firstLabel];
[self.view addSubview:self.secondLabel];
[self.view addSubview:self.tapButton];
self.presenter = [Presenter new];
[self.presenter attachView:self];
}
- (void)buttonClicked{
[self.presenter reloadView];
}
- (void)setFirst:(NSString *)first{
self.firstLabel.text = first;
}
- (void)setSecond:(NSString *)second{
self.secondLabel.text = second;
}
Presenter
.h
@interface Presenter : NSObject
- (void)attachView:(id <MyProtocol>)attachView;
- (void)reloadView;
@end
.m
@interface Presenter()
@property (nonatomic, weak) id <MyProtocol> view;
@property (nonatomic, strong) Model *model;
@end
@implementation Presenter
- (instancetype)init
{
self = [super init];
if (self) {
self.model = [Model new];
self.model.first = @"first";
self.model.second = @"second";
}
return self;
}
- (void)attachView:(id<MyProtocol>)attachView{
self.view = attachView;
}
- (void)reloadView{
//可以在这里做一些数据处理
[self.view setFirst:self.model.first];
[self.view setSecond:self.model.second];
}
@end
这里只是一个简单的Demo,其实思想很简单,就是讲业务逻辑交给Presenter,而Presenter以命令的形式来控制View。
完整Demo可以看这里
一些说明:
MVP架构拥有三个真正独立的分层,所以在组装的时候会有一些问题,而MVP也成了第一个披露这种问题的架构,因为我们不想让View知道Model的信息,所以在当前的Controller去组装是不正确的,我们应该在另外的地方完成组装。比如我们可以创建一个应用层的Router服务,让它来负责组装和View-to-View的转场。这个问题下很多模式中都存在。
下面总结一下MVP的各方面表现:
- 划分——我们把大部分职责都分配到了Presenter和Model里面,而View基本不需要做什么
- 可测性——我们可以通过View来测试大部分业务逻辑
- 易用——代码量差不多是MVC架构的两倍,但是MVP的思路还是蛮清晰的
另外,MVP还有一个变体,它的不同主要就是添加了数据绑定。这个版本的MVP的View和Model直接绑定,而Presenter仍然继续处理View上的用户操作,控制View的显示变化。这种架构和传统的MVC类似,所以我们基本可以舍弃。
MVVM
MVVM可以说是MV(X)系列中最新兴起的也是最出色的一种架构,而它也广受我们iOS程序员喜爱。
MVVM和MVP很像:
- 把ViewController看成View
- View和Model之间没有紧耦合
另外它还让VIew和ViewModel做了数据绑定。ViewModel可以调用对Model做更改,也可以再Model更新的时候对自身进行调整,然后通过View和ViewModel之间的绑定,对View进行相应的更新。
关于绑定
在iOS平台上面有KVO和通知,但是用起来总是觉得不太方便,所以有一些三方库供我们选择:
- 基于KVO的绑定库,如 RZDataBinding 或者 SwiftBond
- 使用全量级的 函数式响应编程 框架,比如ReactiveCocoaRxSwift 或者PromiseKit
实际上,我们在提到MVVM的时候就很容易想到ReactiveCocoa,它也是我们在iOS中使用MVVM的最好工具。但是相对来说它的学习成本和维护成本 也是比较高的,而且一旦你应用不当,很可能造成灾难性的问题。
下面我暂时不用RAC来简单展示一下MVVM:
界面很简单,就是点击一个button修改label里面的数据
Model
@interface MVVMModel : NSObject
@property (nonatomic, copy) NSString *text;
@end
@implementation MVVMModel
- (NSString *)text{
_text = [NSString stringWithFormat:@"newText%d",rand()];
return _text;
}
ViewModel
@interface MVVMViewModel : NSObject
- (void)changeText;
@end
@interface MVVMViewModel()
@property (nonatomic, strong) NSString *text;
@property (nonatomic, strong) MVVMModel *model;
@end
@implementation MVVMViewModel
- (instancetype)init
{
self = [super init];
if (self) {
self.model = [MVVMModel new];
}
return self;
}
- (void)changeText{
self.text = self.model.text;;
}
Controller
@interface MVVMViewController ()
@property (weak, nonatomic) IBOutlet UILabel *textLabel;
@property (nonatomic, strong) MVVMViewModel *viewModel;
@end
@implementation MVVMViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.viewModel = [[MVVMViewModel alloc]init];
[self.viewModel addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];
}
- (IBAction)buttonClicked:(UIButton *)sender {
[self.viewModel changeText];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
self.textLabel.text = change[@"new"];
}
MVVM的核心就是View和ViewModel的一个绑定,这里我只是简单的通过KVO实现,看起来并不是那么优雅,想要深度使用的话我觉得还是有必要学习一下RAC的,需要完整的Demo请看这里。
下面我们再来对MVVM的各方面表现做一个评价:
- 划分——MVVM 框架里面的 View 比 MVP 里面负责的事情要更多一些。因为前者是通过 ViewModel 的数据绑定来更新自身状态的,而后者只是把所有的事件统统交给 Presenter 去处理就完了,自己本身并不负责更新。
- 可测性—— 因为 ViewModel 对 View 是一无所知的,这样我们对它的测试就变得很简单。View 应该也是能够被测试的,但是可能因为它对 UIKit 的依赖,你会直接略过它。
- 易用——它比MVP会更加简洁,因为在 MVP 下你必须要把 View 的所有事件都交给 Presenter 去处理,而且需要手动的去更新 View 的状态;而在 MVVM 下,你只需要用绑定就可以解决。
综上:MVVM 真的很有魅力,因为它不仅结合了上述几种框架的优点,还不需要你为视图的更新去写额外的代码(因为在 View 上已经做了数据绑定),另外它在可测性上的表现也依然很棒。
为了简单易懂,以上的Demo都非常简洁,不知道看了这篇博客能否加深你对MV(X)的一些理解,这些理解也仅作为我个人的一些参考,有什么不对的地方希望大家指出。