前言
高内聚低耦合:每个模块只完成系统要求的独立子功能,并且与其他模块的联系最少且接口简单。
- MVC/MVCS里的V代表View
- MVP/MVVM/MVVM-C/VIPER里面的V代表View/ViewController
MVC/MVCS/MVP/MVVM/MVVM-C我们称之为MV(X)架构 MV(X)框架和VIPER框架的区别如下
1. MVC
M应该做的事:
- 给ViewController提供数据
- 给ViewController存储数据提供接口
- 提供经过抽象的业务基本组件,供Controller调度
C应该做的事:
- 管理View Container的生命周期
- 负责生成所有的View实例,并放入View Container
- 监听来自View与业务有关的事件,通过与Model的合作,来完成对应事件的业务
V应该做的事:
- 响应与业务无关的事件,并因此引发动画效果,点击反馈(如果合适的话,尽量还是放在View去做)等
- 界面元素表达
Model
@interface MModel : NSObject
@property(copy,nonatomic) NSString* content;
@end
@implementation MModel
@end
View
#import "MModel.h"
@interface MView : UIView
@property (strong,nonatomic) UILabel *contentLabel;
@property (strong,nonatomic) MModel *model;
@end
@implementation MView
- (instancetype)init
{
self = [super init];
if(self)
{
_contentLabel = [UILabel new];
[self addSubview:_contentLabel];
_contentLabel.text = @"你好";
_contentLabel.frame = CGRectMake(0, 0, 200, 100);
}
return self;
}
- (void)setModel:(MModel *)model
{
_model = model;
_contentLabel.text = model.content;
}
@end
Controller
#import "ModelVC.h"
#import "MView.h"
@interface ModelVC : UIViewController
@property (nonatomic, strong) MModel *model;
@end
@implementation ModelVC
- (void)viewDidLoad {
[super viewDidLoad];
self.model = [MModel new];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor redColor];
MView *mView = [MView new];
mView.backgroundColor = [UIColor whiteColor];
mView.frame = CGRectMake(100, 100, 200, 100);
[self.view addSubview:mView];
model.content = @"哈";
mView.contentLabel.text = model.content;
UIButton *backBtn = [UIButton new];
[backBtn setTitle:@"Back" forState:UIControlStateNormal];
backBtn.frame = CGRectMake(50, 50,100, 50);
[backBtn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[self.view addSubview:backBtn];
[backBtn addTarget:self action:@selector(touchBack) forControlEvents:UIControlEventTouchUpInside];
}
- (void)touchBack
{
[self dismissViewControllerAnimated:true completion:^{
}];
}
@end
MVC今生(现实情况),View和Model之间也会相互依赖,导致代码耦合复用能力低。
2. 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也不会大到哪儿去。
说回来,MVCS是基于瘦Model的一种架构思路,把原本Model要做的很多事情中的其中一部分关于数据存储的代码抽象成了Store,在一定程度上降低了Controller的压力。
3. MVP
Model-View-Presenter用一个Presenter,把Controller中View的部分剔除,实现了View和Model的隔绝。各部分分工如下:
- View负责界面展示和布局管理,向Presenter暴露视图更新和数据获取的接口,这里包含View/ViewController
- Presenter负责接收来自View的事件,通过View提供的接口更新视图,并管理Model
- Model和MVC中的一样,提供数据模型和数据操作
但是随着界面越来越复杂,Presenter中的业务代码也会越来越庞大,总有一天会遇到一个新的问题:如何再细分Presenter。
Model
@interface Person : NSObject
@property(copy,nonatomic) NSString *name;
@end
@implementation Person
@end
View
#import "PresenterPro.h"
@interface PVC : UIViewController<GreetingView>
@property(nonatomic,strong) id<GreetingViewPresenter> presenter;
@property(strong,nonatomic) UIButton* showGreetingBtn;
@property(strong,nonatomic) UILabel* tipLabel;
@end
@interface PVC ()
@end
@implementation PVC
- (void)viewDidLoad {
[super viewDidLoad];
self.presenter = [[GreetingPresenter alloc] initWithView:self person:[[Person alloc] init]];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];
UIButton *showBtn = [[UIButton alloc]init];
showBtn.frame = CGRectMake(100, 100, 200, 100);
[showBtn setTitle:@"点击测试" forState:UIControlStateNormal];
[showBtn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
[self.view addSubview:showBtn];
[showBtn addTarget:self action:@selector(beginGreet) forControlEvents:UIControlEventTouchUpInside];
UILabel *contentLabel = [UILabel new];
contentLabel.frame = CGRectMake(100, 400, 200, 100);
contentLabel.textColor = [UIColor greenColor];
[self.view addSubview:contentLabel];
_tipLabel = contentLabel;
UIButton *backBtn = [UIButton new];
[backBtn setTitle:@"Back" forState:UIControlStateNormal];
backBtn.frame = CGRectMake(50, 50,100, 50);
[backBtn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[self.view addSubview:backBtn];
[backBtn addTarget:self action:@selector(touchBack) forControlEvents:UIControlEventTouchUpInside];
}
- (void)touchBack
{
[self dismissViewControllerAnimated:true completion:^{
}];
}
- (void)beginGreet
{
[self.presenter showGreeting];;
}
- (void)setGreeting:(NSString *)greeting
{
_tipLabel.text = greeting;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
Presenter
#import "Person.h"
@protocol GreetingView <NSObject>
- (void)setGreeting:(NSString*)greeting;
@end
@protocol GreetingViewPresenter <NSObject>
- (instancetype)initWithView:(id<GreetingView>)view person:(Person*)person;
- (void)showGreeting;
@end
#import "PresenterPro.h"
@interface GreetingPresenter : NSObject<GreetingViewPresenter>
@property(strong,nonatomic) id<GreetingView> view;
@property(strong,nonatomic) Person *person;
@end
@implementation GreetingPresenter
- (instancetype)initWithView:(id)view person:(Person *)person
{
self = [super init];
if(self)
{
_view = view;
_person = person;
}
return self;
}
- (void)showGreeting
{
NSString *hello = [NSString stringWithFormat:@"你们好,我是%@",_person.name];
[_view setGreeting:hello];
}
@end
4. MVVM/MVVM-C
Model-View-ViewModel模式,它也和MVP一样,目的是解决View和Model的耦合。各部分分工如下:
- Model提供数据模型
- View负责视图展示,响应用户交互事件,View这里表示View/ViewController
- ViewModel用于从Model层 获取数据,同步数据到Model中,向View层提供接口,其中包括 数据接口(格式化的数据) 以及事件处理接口,通知View层数据改变,业务逻辑,页面跳转
- 数据绑定可以使MVVM架构更加强大,绑定不是发生在视图(View)和模型(Model)之间,而是视图(View)和视图模型(View Model)之间。
绑定
绑定我在讲解MVP架构部分简单的提到过,这里我们在对其进行一些讨论。绑定是从OSX开发而来的,而且iOS中并没有这个概念。当然,我们有KVO和通知(notifications),但是它的使用并没有绑定方便。 所以,倘若不想自己编写绑定代码,我们还有两个选择:
- 一个是,基于KVO的绑定库,像RZDataBinding或者SwiftBond。
- 完全的函数式响应式编程野兽,像ReactiveCocoa、RxSwift或者PromiseKit`。
事实上,现今,只要你听到“MVVM”你就会想到ReactiveCocoa,反之亦然。尽管使用简单地绑定也可以创建MVVM架构的项目,但是,ReactiveCocoa(或者同类的库)却可以让你把使用MVVM架构的优势最大化。
介于MVVM ViewModel担任了太多的任务,有人提出MVVM几个以下的缺点:
- 繁重的ViewModel代替了繁重的C
- 不可复用的ViewModel代替了不可复用的C
- 没有数据绑定工具的MVVM代码将会变得非常复杂,MVVM的实际重心在哪?如果是数据绑定那么数据绑定为什么在MVVM架构中没有体现。
优化改进方式:
- Daniel Hall提出可以通过将viewModel按照Data Source,Binding,Responder三类功能进一步细分,这么做可能会导致架构与传统的MVVM结构工程结构上看起来有点不一样,功能细化了一定程度上可以提高ViewModel的复用性。
- 使用MVVM-C架构,解耦ViewModel之间的跳转依赖,剥离ViewModel中的页面跳转逻辑,来提高ViewModel的复用性。
MVVM-C就是实现一个Coordinator,将ViewModel中的页面跳转逻辑剥离到Coordinator来解耦,来提高ViewModel的复用性。
Coordinator:主要负责流程和导航。
Coordinator 主要职责:
- 创建并展示新的 ViewController, 以及为该 ViewController 创建 ViewModel 并提供外部依赖项。
- 需要导航时作为 ViewController 方法的委托。例如: didFinish(), showDetail(), openUrl(),…
- 创建新的子 Coordinator 作为导航流程的一部分。
- 使用网络或者本地存储等 Service 获取数据用于导航。最好应该注入 Service 以使其更易于测试。
- 处理 URI 导航
- 存储数据并在模块之间进行传输
Coordinator 不能做的:
- 修改数据
- 改变用户界面
- 直接使用UI
使用MVVM-C模式的好处如下:
- 解耦: 职责的分离更加清晰,也就是说,当您想要更改与应用程序跳转相关的内容时,Coordinator 是惟一需要修改的组件。
- 可测试性: 由于所有 Coordinator 调用都指向一个可替换的 Coordinator,因此 ViewModel 变得更加容易测试。
- 更改导航: 每个 Coordinator 只负责一个组件,并且没有与其父组件有任何假设。因此,它可以放在我们想放的任何地方。
- 代码重用: 除了可以更改导航,还可以重用组件。例如,可以从应用程序中的多个点调用相同的 Coordinator。
Model
@interface Animate : NSObject
@property(copy,nonatomic) NSString *name;
@end
@implementation Animate
@end
View
#import "GreetingViewModel.h"
#import "Animate.h"
@interface MVVMVC : UIViewController
@property(strong,nonatomic) UIButton* showGreetingBtn;
@property(strong,nonatomic) UILabel* tipLabel;
@property(strong,nonatomic) GreetingViewModel *viewModel;
@end
@implementation MVVMVC
- (void)viewDidLoad {
[super viewDidLoad];
self.viewModel = [[GreetingViewModel alloc] initWithAnimate:[[Animate alloc] init]];
[self.viewModel bindModel:[[Animate alloc] init]];
// View绑定ViewModel
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];
UIButton *showBtn = [[UIButton alloc]init];
showBtn.frame = CGRectMake(100, 100, 200, 100);
[showBtn setTitle:@"点击测试" forState:UIControlStateNormal];
[showBtn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
[self.view addSubview:showBtn];
[showBtn addTarget:self.viewModel action:@selector(showGreeting) forControlEvents:UIControlEventTouchUpInside];
UILabel *contentLabel = [UILabel new];
contentLabel.frame = CGRectMake(100, 400, 200, 100);
contentLabel.textColor = [UIColor greenColor];
[self.view addSubview:contentLabel];
_tipLabel = contentLabel;
UIButton *backBtn = [UIButton new];
[backBtn setTitle:@"Back" forState:UIControlStateNormal];
backBtn.frame = CGRectMake(50, 50,100, 50);
[backBtn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[self.view addSubview:backBtn];
[backBtn addTarget:self action:@selector(touchBack) forControlEvents:UIControlEventTouchUpInside];
}
- (void)touchBack
{
[self dismissViewControllerAnimated:true completion:^{
}];
}
- (void)setViewModel:(GreetingViewModel *)viewModel
{
_viewModel = viewModel;
__weak typeof(self) weakSelf = self;
_viewModel.change = ^(GreetingViewModel *model){
weakSelf.tipLabel.text = model.greeting;
};
}
@end
ViewModel
#import "Animate.h"
@interface GreetingViewModel : NSObject
@property(strong,nonatomic) Animate *animate;
@property(copy,nonatomic) NSString *greeting;
@property(copy,nonatomic) void (^change)(GreetingViewModel*);
- (instancetype)initWithAnimate:(Animate*)animate;
- (void)showGreeting;
- (void)bindModel:(Animate *)animate;
@end
@implementation GreetingViewModel
- (instancetype)initWithAnimate:(Animate *)animate
{
self = [super init];
if(self)
{
self.animate = animate;
}
return self;
}
- (void)setGreeting:(NSString *)greeting
{
_greeting = greeting;
self.change(self);
}
- (void)showGreeting
{
self.greeting = [NSString stringWithFormat:@"Hello %@",self.animate.name];
}
- (void)bindModel:(Animate *)animate {
self.animate = animate;
}
@end
5. VIPER
VIPER的全称是View-Interactor-Presenter-Entity-Router。 相比之前的MVX架构,VIPER多出了两个东西:Interactor(交互器)和Router(路由)。
各部分职责如下:
View
- 提供完整的视图,负责视图的组合、布局、更新
- 向Presenter提供更新视图esi的接口
- 将View相关的事件发送给Presenter
Presenter
- 接收并处理来自View的事件
- 向Interactor请求调用业务逻辑
- 向Interactor提供View中的数据
- 接收并处理来自Interactor的数据回调事件
- 通知View进行更新操作
- 通过Router跳转到其他View
Router
- 提供View之间的跳转功能,减少了模块间的耦合
- 初始化VIPER的各个模块
Interactor
- 维护主要的业务逻辑功能,向Presenter提供现有的业务用例
- 维护、获取、更新Entity
- 当有业务相关的事件发生时,处理事件,并通知Presenter
Entity
- 和Model一样的数据模型
和MVX的区别
VIPER把MVC中的Controller进一步拆分成了Presenter、Router和Interactor。和MVP中负责业务逻辑的Presenter不同,VIPER的Presenter的主要工作是在View和Interactor之间传递事件,并管理一些View的展示逻辑,主要的业务逻辑实现代码都放在了Interactor里。Interactor的设计里提出了"用例"的概念,也就是把每一个会出现的业务流程封装好,这样可测试性会大大提高。而Router则进一步解决了不同模块之间的耦合。所以,VIPER和上面几个MVX相比,多总结出了几个需要维护的东西:
- View事件管理
- 数据事件管理
- 事件和业务的转化
- 总结每个业务用例
- 模块内分层隔离
- 模块间通信
而这里面,还可以进一步细分一些职责。VIPER实际上已经把Controller的概念淡化了,这拆分出来的几个部分,都有很明确的单一职责,有些部分之间是完全隔绝的,在开发时就应该清晰地区分它们各自的职责,而不是将它们视为一个Controller。
优点 VIPER的特色就是职责明确,粒度细,隔离关系明确,这样能带来很多优点:
- 可测试性好。UI测试和业务逻辑测试可以各自单独进行。
- 易于迭代。各部分遵循单一职责,可以很明确地知道新的代码应该放在哪里。
- 隔离程度高,耦合程度低。一个模块的代码不容易影响到另一个模块。
- 易于团队合作。各部分分工明确,团队合作时易于统一代码风格,可以快速接手别人的代码。
缺点
- 一个模块内的类数量增大,代码量增大,在层与层之间需要花更多时间设计接口。
使用代码模板来自动生成文件和模板代码可以减少很多重复劳动,而花费时间设计和编写接口是减少耦合的路上不可避免的,你也可以使用数据绑定这样的技术来减少一些传递的层次。
- 模块的初始化较为复杂,打开一个新的界面需要生成View、Presenter、Interactor,并且设置互相之间的依赖关系。而iOS中缺少这种设置复杂初始化的原生方式。
简单来说,就是Cocoa框架缺少一个强大的自定义依赖注入工具。这个问题影响不是特别大,可以选用一些第三方工具来实现,也可以在Router的界面跳转方法里,对模块进行初始化,只不过总是不够完美。针对这个问题,我实现了一个基于protocol声明依赖的界面跳转Router,将会在之后的文章中进行详解。
总结
有人可能会觉得,一个界面模块真的有必要使用这么复杂的架构吗?这样是不是过度设计?
我反对这种观点。不要被VIPER的组织图吓到,VIPER并不复杂,它是将原来MVC中的Controller中的各种任务进行了清晰的分解,在写代码时,你会很清楚你正在做什么。事实上,它比使用了数据绑定技术的MVVM更加简单,就是因为它职责明确。从MVC转到VIPER的过程同样是很清晰的,它甚至把重构的思路都体现出来了。而MVVM则留下了许多尚未明确的责任,导致不同的人会在某些地方有不同的实现。即便你还在使用MVC,你也应该在Controller中分离出VIPER总结出的那些专项职责,既然如此,为何不彻底地明确这些职责,把它们分散到不同的文件中呢?一旦开始这样的工作,你就已经向VIPER靠拢了。
有人可能会觉得,VIPER适合大型app,中小型app没必要过早使用。
我反对这种观点。VIPER是单个界面模块内的架构设计,并不是整个app架构层面的设计,和app的整体架构没有多大的关系,也不存在过早使用VIPER的情况。所以,严格来说,是复杂界面更适合VIPER,而不是大型app更适合VIPER。
至此,我的结论就是,快点拥抱VIPER的怀抱吧。