浅入浅出VIPER设计架构(1)

之前和豌豆荚的同学聊天,发现他们的App从MVVM切换到VIPER设计架构,于是这两天花了点时间研读了一下 相关资料,浅谈一下。

VIPER是什么

VIPER是View, Interactor, Presenter, Entity, Routing5个词语的缩写,这5个词语也是VIPER架构的核心。

View - 视图,接收传递的内容,然后进行内容展示。

Presenter - 视图逻辑控制器,这个模块只用来执行和UI相关的逻辑。什么叫和UI相关的逻辑呢?比如:接收用户交互、将展示的内容分发给不同的UI界面进行展示。

Interactor - 纯业务逻辑控制器,这个模块完全脱离于UI,只用来做业务相关的逻辑控制。举例来说,比如下载逻辑、计算逻辑等等可以划分为单一Use Case的逻辑。

Entity - 基础的数据模型,比方说大家经常定义的XXXModel之类的,但是,这里的XXXModel可以认为基本就是纯数据结构了,不包含轻量级业务处理逻辑

Routing - 路由器,视图切换的逻辑。比如包含如何从一个界面切换到另一个界面,切换的顺序是如何等等。

除了五大核心之外,我们还经常使用了Data Store这个模块。因为我们之前提过,我们的Entity基本就是纯数据结构,不包含轻量级的业务逻辑,因此,Entity进行持久化这个职责是要单独划分出来,而这个职能就落到了Data Store身上。

下面是网上一张比较经典的架构设计图:

为什么要用VIPER

在软件工程领域,有一个很重要的观点,就是测试、测试、测试。(重要的话说三遍!)

之前在BMW Group实习的时候从事Java开发,每一个函数都要经过JUnit的测试,也就是我们所谓的Test-Driven Development。但是iOS开发由于其逻辑和视图的强耦合关系,常常导致业务逻辑不能完全独立于界面,进行单元测试十分困难。比如,臭名昭著的”Massive View Controller” 就是因为将大量的业务逻辑和视图逻辑耦合进了Controller,导致Controller非常臃肿,也不容易剥离进行测试。

而VIPER架构引入了Interactor和Presentator两个概念,将业务逻辑和视图逻辑独立开来,从而可以单独测试业务逻辑的代码。

例子分析

说了好多虚的概念,有些人一定已经被弄晕了,还是赶紧看两个典型的例子看深入了解下吧.

1. Counter

第一个例子来源于Counter,是一个非常简单了计数应用。麻雀虽小,却五脏俱全。

打开项目,我们可以快速的过一下项目结构:

-- CNTAppDelegate.h/.m
-- CNTCountInteractorIO.h
-- CNTCountInteractor.h/.m
-- CNTCountPresenter.h/.m
-- CNTCountView.h
-- CNTCountViewController.h/.m/.xib

首先我们来看一下最简单的模块:CNTCountView

// CNTCountView.h
@protocol CNTCountView <NSObject>
- (void)setCountText:(NSString*)countText;
- (void)setDecrementEnabled:(BOOL)enabled;
@end

咦?为什么这个View只包含一个Protocol呢?说好的View用来接收传递的内容然后进行内容展示呢?

原因是这样:在iOS应用中,一个无法避免的模块就是ViewController(无论如何每个应用都必须存在一个RootViewController)。每个ViewController包含一个根View用来进行视图展示。因此,在将VIPER架构应用到iOS领域的过程中,实际上起到View作用的是ViewController!(当然,ViewController不仅仅起到了视图展示的作用)

题外话:我个人认为这个例子的取名不太好,让人迷惑,如果换个名字,如CNTCountViewOperation就没有这个问题了

既然如此,让我们赶快去看看相关的ViewController:CNTCountViewController中的实现。

// CNTCountViewController.h
@interface CNTCountViewController : UIViewController <CNTCountView>
@property (nonatomic, weak) IBOutlet    UILabel*    countLabel;
@property (nonatomic, weak) IBOutlet    UIButton*   decrementButton;
@property (nonatomic, weak) IBOutlet    UIButton*   incrementButton;

@property (nonatomic, strong)   CNTCountPresenter*  presenter;
@end

在上述CNTCountViewController的头文件中,我们可以看到该类遵从了CNTCountView协议。其次,他包含了真正的View:两个UIButton和一个UILabel。说明这和我们之前说的ViewController起到了真正的视图展示作用是吻合的。

同时,我们还看到了一个非常显眼的类:CNTCountPresenter。如果这个类的命名没错,那这个类就是所谓的Presenter了,用来进行界面逻辑控制的类。那么是不是这样呢?我们Command + 左击这个类一探究竟!

// CNTCountPresenter.h
@interface CNTCountPresenter : NSObject <CNTCountInteractorOutput>
@property (nonatomic, weak)     id<CNTCountView>            view;
@property (nonatomic, strong)   id<CNTCountInteractorInput> interactor;

- (void)updateView;
- (void)increment;
- (void)decrement;
@end

从CNTCountPresenter的头文件中,我们能看出很多逻辑关系。Presenter包含了一个View和一个Interactor。这和文首我们对于VIPER的介绍相吻合:Presenter从Interactor获取数据,经过界面逻辑处理后,将数据发送给View进行展示。

从Interactor请求获取数据的部分代码如下:

// CNTCountPresenter.m
- (void)updateView
{
    [self.interactor requestCount];
}

- (void)increment
{
    [self.interactor increment];
}

- (void)decrement
{
    [self.interactor decrement];
}

由于Presenter从Interactor获取数据,那么势必Presenter是Interactor的一个输出,而Interactor是Presenter的一个输入。

// CNTCountInteractorIO.h
@protocol CNTCountInteractorInput <NSObject>
- (void)requestCount;
- (void)increment;
- (void)decrement;
@end

@protocol CNTCountInteractorOutput <NSObject>
- (void)updateCount:(NSUInteger)count;
@end

这里各位可以先不关注Protocol协议的设计本身,只要理解逻辑关系即可。

通过这样的职责划分,落入到Interactor类中的职责就是最基本的业务逻辑:计数的增加和减少。

// CNTCountInteractor.h
- (void)requestCount
{
    [self sendCount];
}

- (void)increment
{
    ++self.count;
    [self sendCount];
}

- (void)decrement
{
    if ([self canDecrement])
    {
        --self.count;
        [self sendCount];
    }
}

至此,我们可以将Counter的项目架构进行如下表示:

Interactor <——–> Presenter

  • InteractorPresenter输入
  • PresenterInteractor输出
  • Presenter 通过某些事件触发,去向其输入Interactor请求数据

Presenter <——–> View(ViewController)

  • ViewPresenter输出
  • ViewController 触发了 PresenterInteractor中请求新的业务结果,从而更新View

Presenter更新View的代码如下:

// CNTCountPresenter.m
- (void)updateCount:(NSUInteger)count
{
    [self.view setCountText:[self formattedCount:count]];
    [self.view setDecrementEnabled:[self canDecrementCount:count]];
}

到这里,Counter这个例子的解读基本就完成了。细心的读者一定会发现,EntityRoute到哪里去了?

是的,这个例子由于过于简单,压根不需要Entity。并且由于它是单视图应用,压根不涉及到页面切换,因此也无须Route功能。不过我们在后续的浅入浅出VIPER架构(2)中解读一个更细致更负责的例子,敬请期待!