iOS其他节选 ios零碎记录iOS 开发继续加油

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

2018-04-28  本文已影响497人  wg689
图片和文字无关

前言
谁不希望自己写的代码像美女一样养眼,那我们聊聊架构吧,学好了架构才能让别的程序员"干"的爽

一). 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不得不承担大量的工作:

二) 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需要如下三步:

@protocol CNTCountInteractorOutput <NSObject>
- (void)updateCount:(NSUInteger)count;
@end
@interface CNTCountPresenter : NSObject <CNTCountInteractorOutput>
//  以前需要在这里暴露 现在通过遵守协议的方式 - (void)updateCount:(NSUInteger)count;
@end
  CNTCountInteractor* interactor = [[CNTCountInteractor alloc] init];
   interactor.output = presenter;

详细的例子参考:协议之间通信的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。

觉得不错就点个👍.gif

参考文献

iOS 架构模式--解密 MVC,MVP,MVVM以及VIPER架构
iOS MVVM架构
iOS VIPER架构实践(一):从MVC到MVVM到VIPER
MVVM与Controller瘦身实践

上一篇下一篇

猜你喜欢

热点阅读