iOS设计模式MVC、MVCS、MVP、MVVM/MVVM-C、VIPER

1,546 阅读15分钟

前言

高内聚低耦合:每个模块只完成系统要求的独立子功能,并且与其他模块的联系最少且接口简单。

  • 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的怀抱吧。