iOS中的VIPER和MVC,MVVM 架构之间的比较

一). iOS架构的分类

在iOS中架构有很多,最常用的MVC,MVVM,MVP, 不常用的有VIPER

1.1)传统的MVC的架构是这样的:

传统MVC的架构

上面是传统的MVC的架构虽说耦合极端严重,这也是在项目中非常常见的一种架构形式,很多公司在使用这个模式,三个实体间相互都有通信,而且是紧密耦合的。这很显然会大大降低了三者的复用性,而这正是我们不愿意看到的。很容易造成几千行上万行的控制器,苹果希望的MVC的架构实际上不应该是上述的MVC的架构,希望的是这样的效果:

1.2) 苹果希望的MVC的架构是这样的:

苹果希望的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架构

MVVM架构.png

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架构


VIPER架构.png

相比之前的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 这样造成了不必要的耦合,这是常规的头文件包含的方式,这样在后期无法拆分代码和重构 ,整个项目会造成一种 网状的关系

网状的结构.jpeg

上面的网状项目在实际项目中很常见,解耦也比较困难,为了相对解耦或者为了未来更加方便解耦或者拆分使用一种协议的方式解耦,解耦方法如下:比如让A 和B 之间实现解耦:
interator 传递事件给presenter需要如下三步:

    1. 声明协议
@protocol CNTCountInteractorOutput <NSObject>
- (void)updateCount:(NSUInteger)count;
@end
    1. 遵守协议
@interface CNTCountPresenter : NSObject <CNTCountInteractorOutput>
//  以前需要在这里暴露 现在通过遵守协议的方式 - (void)updateCount:(NSUInteger)count;
@end
    1. 设置属性保存, interactor和presenter建立关系
  CNTCountInteractor* interactor = [[CNTCountInteractor alloc] init];
   interactor.output = presenter;
    1. 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的模块

觉得不错就点个👍.gif

参考文献

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 配置图文教程及坑总结

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 148,827评论 1 317
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 63,511评论 1 266
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 99,318评论 0 218
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,108评论 0 189
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,112评论 1 266
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,387评论 1 185
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,905评论 2 283
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,657评论 0 177
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,144评论 0 223
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,744评论 2 225
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,100评论 1 236
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,565评论 2 222
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,041评论 3 216
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,769评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,301评论 0 178
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,173评论 2 239
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,313评论 2 242

推荐阅读更多精彩内容