一). iOS架构的分类
在iOS中架构有很多,最常用的MVC,MVVM,MVP, 不常用的有VIPER
1.1)传统的MVC的架构是这样的:
上面是传统的MVC的架构虽说耦合极端严重,这也是在项目中非常常见的一种架构形式,很多公司在使用这个模式,三个实体间相互都有通信,而且是紧密耦合的。这很显然会大大降低了三者的复用性,而这正是我们不愿意看到的。很容易造成几千行上万行的控制器,苹果希望的MVC的架构实际上不应该是上述的MVC的架构,希望的是这样的效果:
1.2) 苹果希望的MVC的架构是这样的:
由于Controller是一个介于View 和 Model之间的协调器,所以View和Model之间没有任何直接的联系, 这也是相对于传统的MVC的变化,减少了View和Model之间的通信,。Controller是一个最小可重用单元,这对我们来说是一个好消息,因为我们总要找一个地方来写逻辑复杂度较高的代码,而这些代码又不适合放在Model中。
这样还是不是不适合单元测试,同时控制器还是很臃肿因此有人说Massive View Controller
1.3) MVC的弊端
通过上图,我们可以看到在纯粹的MVC设计模式中,Controller不得不承担大量的工作:
- 网络API请求
- 数据读写
- 日志统计
- 数据的处理(JSON<=>Object,数据计算)
- 对View进行布局,动画
- 处理Controller之间的跳转(push/modal/custom)
- 处理View层传来的事件,返回到Model层
- 监听Model层,反馈给View层
于是,大量的代码堆积在Controller层中,MVC最后成了Massive View Controller(重量级视图控制器)。
为了解决这种问题,我们通常会为Controller瘦身,也就是把Controller中代码抽出到不同的类中,引入MVVM就是为Controller瘦身的一个很好的实践。
二) MVVM架构
Controller中我们不需要再做多余的判断,那些表示逻辑我们已经移植到了ViewModel中,ViewController明显轻量了很多。ViewModel承担了部分控制器的业务,因此可以比较好的减轻控制器的负担
2.1) MVC和MVVM代码比较的差异
比如我们有一个需求:一个页面,需要判断用户是否手动设置了用户名。如果设置了,正常显示用户名;如果没有设置,则显示“简书0122”这种格式。(虽然这些本应是服务器端判断的)
我们看看MVC和MVVM两种架构都是怎么实现这个需求的
2.2) MVC 代码实例
Model类:
#import <Foundation/Foundation.h>
@interface User : NSObject
@property (nonatomic, copy) NSString *userName;
@property (nonatomic, assign) NSInteger userId;
@end
ViewController类:
#import "HomeViewController.h"
#import "User.h"
@interface HomeViewController ()
@property (nonatomic, strong) UILabel *lb_userName;
@property (nonatomic, strong) User *user;
@end
@implementation HomeViewController
- (void)viewDidLoad {
[super viewDidLoad];
if (_user.userName.length > 0) {
_lb_userName.text = _user.userName;
} else {
_lb_userName.text = [NSString stringWithFormat:@"简书%ld", _user.userId];
}
}
这里我们需要将表示逻辑也放在ViewController中。
MVVM:
Model类:
#import <Foundation/Foundation.h>
@interface User : NSObject
@property (nonatomic, copy) NSString *userName;
@property (nonatomic, assign) NSInteger userId;
@end
ViewModel类:
声明:
#import <Foundation/Foundation.h>
#import "User.h"
@interface UserViewModel : NSObject
@property (nonatomic, strong) User *user;
@property (nonatomic, copy) NSString *userName;
- (instancetype)initWithUser:(User *)user;
@end
实现:
#import "UserViewModel.h"
@implementation UserViewModel
- (instancetype)initWithUser:(User *)user {
self = [super init];
if (!self) return nil;
_user = user;
if (user.userName.length > 0) {
_userName = user.userName;
} else {
_userName = [NSString stringWithFormat:@"简书%ld", _user.userId];
}
return self;
}
@end
Controller类:
#import "HomeViewController.h"
#import "UserViewModel.h"
@interface HomeViewController ()
@property (nonatomic, strong) UILabel *lb_userName;
@property (nonatomic, strong) UserViewModel *userViewModel;
@end
@implementation HomeViewController
- (void)viewDidLoad {
[super viewDidLoad];
_lb_userName.text = _userViewModel.userName;
}
可见,Controller中我们不需要再做多余的判断,那些表示逻辑我们已经移植到了ViewModel中,ViewController明显轻量了很多。MVVM还有另一个问题。把业务逻辑放到ViewModel中,虽然能够为UIViewController减负,但是只是把问题转移了,最终ViewModel还是会变成另一个Massive ViewModel。
三) VIPER架构的介绍
上面的ViewModel处理了很多控制器的业务,不是很适合做单元测试,还是会导致Massive ViewModel,为了解决 Massive ViewModel需要对职责进行进一步划分,那就是VIPER
VIPER的全称是View-Interactor-Presenter-Entity-Router,据笔者了解豆瓣和Uber的iOS技术团队已经在使用VIPER架构
相比之前的MVX架构,VIPER多出了两个东西:Interactor(交互器)和Router(路由)。
3.1) VIPER各部分职责如下:
View
提供完整的视图,负责视图的组合、布局、更新
向Presenter提供更新视图的接口
将View相关的事件发送给Presenter
Presenter
接收并处理来自View的事件
向Interactor请求调用业务逻辑
向Interactor提供View中的数据
接收并处理来自Interactor的数据回调事件
通知View进行更新操作
通过Router跳转到其他View
Router
提供View之间的跳转功能,减少了模块间的耦合
初始化VIPER的各个模块
Interactor
维护主要的业务逻辑功能,向Presenter提供现有的业务用例
维护、获取、更新Entity
当有业务相关的事件发生时,处理事件,并通知Presenter
Entity和Model一样的数据模型
3.2) VIPER之间的通信方式(协议)
常规的通信方式是A 模块需要调用B 模块的 方法或者属性,直接在B 模块的中的方法暴露出头文件 中,如下面的方法所示:
@interface CNTCountPresenter : NSObject
- (void)updateCount:(NSUInteger)count;
- (void)updateCountAModel:(AModel)count;
- (void)updateCountBModel:(BModel)count;
- (void)updateCountCModel:(CModel)count;
- (void)updateCountDModel:(DModel)count;
@end
上面的方式来实现协议的调用的时候,需要导入文件,这样本来只需要- (void)updateCountAModel:(AModel)count;
这个方法,结果导入了BModel, CModel, DModel
这样造成了不必要的耦合,这是常规的头文件包含的方式,这样在后期无法拆分代码和重构 ,整个项目会造成一种 网状的关系
上面的网状项目在实际项目中很常见,解耦也比较困难,为了相对解耦或者为了未来更加方便解耦或者拆分使用一种协议的方式解耦,解耦方法如下:比如让A 和B 之间实现解耦:
interator 传递事件给presenter需要如下三步:
- 声明协议
@protocol CNTCountInteractorOutput <NSObject>
- (void)updateCount:(NSUInteger)count;
@end
- 遵守协议
@interface CNTCountPresenter : NSObject <CNTCountInteractorOutput>
// 以前需要在这里暴露 现在通过遵守协议的方式 - (void)updateCount:(NSUInteger)count;
@end
- 设置属性保存, interactor和presenter建立关系
CNTCountInteractor* interactor = [[CNTCountInteractor alloc] init];
interactor.output = presenter;
-
- interactor 中调用协议方法
[self.output updateCount:self.count];
详细的例子参考:协议之间通信的demo
3.3)协议方式解耦
上述的模块之间的通信通过协议实现,可以实现最大的限度的解耦,在直播间重构中也被广泛使用是一种比较好的解耦方式,无论是MVC 和MVVM,MVP 还是VIPER 及其各种 变种的MVVM,变种的VIPER 都需要解决模块之间的通信,都可以借鉴协议的方式来进行解耦,协议来实现解耦在网络通信中使用比较广泛,网络七层通信协议,其实在iOS 模块化之间也可以大力借鉴
四) VIPER和MVX的区别
VIPER把MVC中的Controller进一步拆分成了Presenter、Router和Interactor。和MVP中负责业务逻辑的Presenter不同,VIPER的Presenter的主要工作是在View和Interactor之间传递事件,并管理一些View的展示逻辑,主要的业务逻辑实现代码都放在了Interactor里。Interactor的设计里提出了"用例"的概念,也就是把每一个会出现的业务流程封装好,这样可测试性会大大提高。而Router则进一步解决了不同模块之间的耦合。所以,VIPER和上面几个MVX相比,多总结出了几个需要维护的东西:
View事件管理
数据事件管理
事件和业务的转化
总结每个业务用例
模块内分层隔离
模块间通信
而这里面,还可以进一步细分一些职责。VIPER实际上已经把Controller的概念淡化了,这拆分出来的几个部分,都有很明确的单一职责,有些部分之间是完全隔绝的,在开发时就应该清晰地区分它们各自的职责,而不是将它们视为一个Controller。
TODO
vipER与大型控制器重构之间的异同点
viper简介
https://github.com/iSame7/ViperCode 产生模块
https://github.com/objcio/issue-13-viper/tree/master/VIPER%20TODO
VIPER 实战
产生VIPER的模块
参考文献
iOS 架构模式--解密 MVC,MVP,MVVM以及VIPER架构
iOS MVVM架构
iOS VIPER架构实践(一):从MVC到MVVM到VIPER
MVVM与Controller瘦身实践
作者开发经验总结的文章推荐,持续更新学习心得笔记
五星推荐 Runtime 10种用法(没有比这更全的了)
五星推荐 成为iOS顶尖高手,你必须来这里(这里有最好的开源项目和文章)
五星推荐 iOS逆向Reveal查看任意app 的界面
五星推荐手把手教你使用python自动打包上传应用分发
JSPatch (实时修复App Store bug)学习(一)
iOS 高级工程师是怎么进阶的(补充版20+点)
扩大按钮(UIButton)点击范围(随意方向扩展哦)
最简单的免证书真机调试(原创)
通过分析微信app,学学如何使用@2x,@3x图片
TableView之MVVM与MVC之对比
使用MVVM减少控制器代码实战(减少56%)
ReactiveCocoa添加cocoapods 配置图文教程及坑总结