ZIKViper 1.1

ZIKViper 1.1

测试已测试
Lang语言 Obj-CObjective C
许可证 MIT
发布最新发布2017年9月

zuik维护。



  • Zuikyo

iOS VIPER实现。展示使用协议发现模块和依赖注入。


两个iOS VIPER架构实现,关注于模块化和基于接口的依赖注入。


目录

VIPER简介

VIPER的全称是View-Interactor-Presenter-Entity-Router。示意图如下:

VIPER

与MVX架构相比,VIPER增加了两个元素:Interactor(交互器)和Router(路由器)。

各部分职责如下:

视图

  • 提供完整的视图,负责视图的组合、布局和更新
  • 向表示器提供更新视图的接口
  • 将视图相关的事件发送给表示器

表示器

  • 接收并处理来自视图的事件
  • 向交互器请求调用业务逻辑
  • 向交互器提供视图中的数据
  • 接收并处理来自交互器的数据回调事件
  • 通知视图进行更新操作
  • 通过路由器跳转到其他视图

路由器

  • 提供视图间的跳转功能,减少模块间的耦合
  • 初始化VIPER的各个模块

交互器

  • 维护主要业务逻辑功能,向表示器提供现有的业务用例
  • 维护、获取、更新实体
  • 当业务相关的事件发生时,处理事件并通知表示器

实体

  • 与Model相同的数据模型

与MVX的区别

VIPER将MVC中的Controller进一步拆分为表示器、路由器和交互器。与MVP中负责业务逻辑的表示器不同,VIPER的表示器的主要工作是在视图和交互器之间传递事件,并管理一些视图的展示逻辑,主要的业务逻辑实现代码都放在交互器里。交互器的设计中提出了"用例"的概念,即把每个业务流程封装好,这样可测试性会大大提高。而路由器进一步解决了不同模块之间的耦合。因此,与上述几个MVX相比,VIPER总结出了几个需要维护的东西:

  • 视图事件管理
  • 数据事件管理
  • 将事件转化为业务
  • 总结每个业务用例
  • 模块内分层隔离
  • 模块间通信

在这其中,还可以进一步细化一些职责。VIPER实际上已经将Controller的概念淡化,这些拆分出来的部分都有很明确的单一职责,有些部分之间是完全隔离的,在开发时应该清晰地定义各自的职责,而不是将它们视为一个Controller。

优点

VIPER的特色是职责明确,粒度细,隔离关系明确,这能带来很多优点:

  • 可测试性好。UI测试和业务逻辑测试可以各自独立进行。
  • 易于迭代。各部分遵循单一职责,可以很明确地知道新的代码应该放在哪里。
  • 隔离度高度,耦合度低。一个模块的代码不太可能影响另一个模块。
  • 易于团队合作。各部分分工明确,团队合作时易于统一代码风格,可以快速接手别人的代码。

起源

VIPER架构,最初是在2013年由Jeff Gilbert 和 Conrad Stoll 在MutualMobile的技术博客上提出的。由于博客迁移,原文链接已经失效。以下是迁移后的博文链接:MEET VIPER: MUTUAL MOBILE’S APPLICATION OF CLEAN ARCHITECTURE FOR IOS APPS

这是文章中提到的架构示意图:

viper-mutualmobile

Wireframe可以被视为Router的另一种表达方式。从图中可以看出,VIPER各组件之间的关系已经很明确了。作者随后于2014年在objc.io上发表了另一篇更详细的介绍文章:Architecting iOS Apps with VIPER

作者的第一篇文章阐述了VIPER是在接触到Uncle Bob的Clean Architecture后,对其进行的一次实践。因此,VIPER的真正源头应该是Clean Architecture。

Clean Architecture

Uncle Bob于2011年提出的Clean Architecture是一种平台无关的抽象架构。想要深入学习,可以阅读原作者的《Clean Architecture》:Clean Architecture,其翻译版本为《干净的架构The Clean Architecture》:干净的架构The Clean Architecture

它通过梳理软件中不同层之间的依赖关系,提出了一种自外向内,单向依赖的架构,如下图所示:

Clean Architecture

越靠近内层,越变得抽象,越接近设计的核心。越靠近外层,越与具体的平台和实现技术相关。内层的部分完全不关心外层的存在和实现方式,代码只能从外层向内层引用,目的是为了实现层与层之间的隔离。将不同抽象程度的层进行隔离,实现了业务规则与具体实现的分离。可以认为外层是内层的delegate,外层只能通过内层提供的delegate接口来使用内层。

企业业务规则

代表了这个软件项目的业务规则,通过数据实体体现,是一些可以在不同的程序应用之间共享的数据结构。

应用程序业务规则

代表了本应用所使用的业务规则。封装并实现了用到的业务功能,会将各种实体的数据结构转换为在用例中传递的实体类,但与具体的数据库技术或UI无关。

接口适配器

接口适配层。将用例的规则与具体的实现技术进行抽象对接,将用例中用到的实体类转换为数据库存储格式或供View显示的格式。类似于MVVM中将Model转换为ViewModel以供View显示。

右下角表示了接口适配层中不同模块间的通信方式。不同的模块在业务用例中产生关联和数据传递。Input、Output就是Use Case提供给外层的数据流接口。

框架与驱动器

库和驱动器层,代表了选用的各种具体的实现技术,例如持久层使用SQLite还是Core Data,网络层使用NSURLSession、NSURLConnection还是AFNetworking等。

总结

从Clean Architecture中可以看到Use Case、Interactor、Presenter等概念的出现,它们为VIPER的工程实现提供了设计思想。VIPER将这些设计转化为具体的实现。VIPER中的各部分正存在由外向内的依赖,表现为从外向内:View -> Presenter -> Interactor -> EntityWireframe严格来说也是一类特殊的Use Case,用于不同模块间的通信,连接了不同的Presenter

必须记住的是,VIPER架构是根据由外向内的依赖关系来设计的。这句话是指导我们进行进一步设计和优化的关键。

方案一:最完整的VIPER

首先总结出一个绝对标准的VIPER,各部分遵循隔离关系,同时考虑到依赖注入、子模块通信、模块间解耦等问题,将VIPER各部分的职责变得更加明确,也新增了几个角色。示例图如下,各角色的颜色与Clean Architecture图中各层的颜色对应:

thorough viper

示例代码将以一个笔记应用作为演示。

View

View可以是一个UIView + UIViewController,也可以只是一个custom UIView,或者一个自定义的用于管理UIView的Manager,只要它实现了View的接口即可。

View层的职责:

  • 展示界面,组合各种UIView,并在UIViewController内管理控件的布局、更新等。
  • View对外暴露各种用于更新UI的接口,而自己不主动更新UI。
  • View持有一个由外部注入的事件处理器对象,将View层的事件发送给事件处理器。
  • View持有一个由外部注入的数据源对象,在View渲染过程中,会从数据源获取一些用于展示的数据,数据源接口的命名应尽量避免与具体业务相关。
  • View向Presenter提供路由源,即用于界面跳转的源界面。

View层会引入各种自定义控件,这些控件有许多代理,它们在View层实现,然后统一封装后交给Presenter层实现。因为Presenter层不知道View的实现细节,所以也就不知道这些控件的接口,Presenter层只知道View层统一暴露出来的接口。而且这些控件的接口在定义时可能会将数据获取、事件回调、控件渲染接口混淆在一起,最具代表性的就是UITableViewDataSource中的-tableView:cellForRowAtIndexPath:。这个接口同时涉及到了UITableViewCell和渲染cell所需要的Model,非常容易产生耦合。因此需要对其进行分解。应该在View的数据源中定义一个从外部获取所需简单类型数据的方法,在-tableView:cellForRowAtIndexPath:中使用获取到的数据渲染cell。示例代码:

@protocol ZIKNoteListViewEventHandler <NSObject>
- (void)handleDidSelectRowAtIndexPath:(NSIndexPath *)indexPath;
@end

@protocol ZIKNoteListViewDataSource <NSObject>
- (NSInteger)numberOfRowsInSection:(NSInteger)section;
- (NSString *)textOfCellForRowAtIndexPath:(NSIndexPath *)indexPath;
- (NSString *)detailTextOfCellForRowAtIndexPath:(NSIndexPath *)indexPath;
@end

@interface ZIKNoteListViewController () <UITableViewDelegate,UITableViewDataSource>
@property (nonatomic, strong) id<ZIKNoteListViewEventHandler> eventHandler;
@property (nonatomic, strong) id<ZIKNoteListViewDataSource> viewDataSource;
@property (weak, nonatomic) IBOutlet UITableView *noteListTableView;
@end

@implementation ZIKNoteListViewController

- (UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath
                                      text:(NSString *)text
                                detailText:(NSString *)detailText {
    UITableViewCell *cell = [self.noteListTableView dequeueReusableCellWithIdentifier:@"noteListCell" forIndexPath:indexPath];
    cell.textLabel.text = text;
    cell.detailTextLabel.text = detailText;
    return cell;
}


#pragma mark UITableViewDataSource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [self.viewDataSource numberOfRowsInSection:section];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *text = [self.viewDataSource textOfCellForRowAtIndexPath:indexPath];
    NSString *detailText = [self.viewDataSource detailTextOfCellForRowAtIndexPath:indexPath];
    UITableViewCell *cell = [self cellForRowAtIndexPath:indexPath
                                                   text:text
                                             detailText:detailText];
    return cell;
}

#pragma mark UITableViewDelegate

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    
    [self.eventHandler handleDidSelectRowAtIndexPath:indexPath];
}

@end

通常,数据源和事件处理器都是由Presenter来扮演的。当Presenter接收到数据源请求时,从Interactor中获取并返回所需数据。你也可以选择在View和Presenter之间使用ViewModel进行交互。

Presenter

Presenter由View持有,其职责包括:

  • 接收并处理来自视图的事件
  • 维护与View相关的各种状态和配置,如是否使用夜间模式等。
  • 调用Interactor提供的Use Case执行业务逻辑。
  • 向Interactor提供View中的数据,让Interactor生成所需的Model。
  • 接收并处理来自Interactor的业务事件回调。
  • 通知视图进行更新操作
  • 通过Wireframe跳转到其他View。

Presenter是View和业务之间的中转站,它不包含业务实现代码,而是负责调用现成的各种Use Case,将具体事件转化为具体业务。Presenter中不应导入UIKit,否则就可能侵入View层的渲染工作。Presenter中也不应出现Model类。当数据从Interactor传递到Presenter时,应将其转换为简单的数据结构。

示例代码:

@interface ZIKNoteListViewPresenter ()
@property (nonatomic, strong) id<ZIKNoteListWireframeProtocol> wireframe;
@property (nonatomic, weak) id<ZIKViperView,ZIKNoteListViewProtocol> view;
@property (nonatomic, strong) id<ZIKNoteListInteractorProtocol> interactor;
@end

@implementation ZIKNoteListViewPresenter

#pragma mark ZIKNoteListViewDataSource

- (NSInteger)numberOfRowsInSection:(NSInteger)section {
    return self.interactor.noteCount;
}

- (NSString *)textOfCellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *title = [self.interactor titleForNoteAtIndex:indexPath.row];
    return title;
}

- (NSString *)detailTextOfCellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *content = [self.interactor contentForNoteAtIndex:indexPath.row];
    return content;
}

#pragma mark ZIKNoteListViewEventHandler

- (void)handleDidSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *uuid = [self.interactor noteUUIDAtIndex:indexPath.row];
    NSString *title = [self.interactor noteTitleAtIndex:indexPath.row];
    NSString *content = [self.interactor noteContentAtIndex:indexPath.row];
    
    [self.wireframe pushEditorViewForEditingNoteWithUUID:uuid title:title content:content delegate:self];
}

@end

Interactor

Ineractor的职责:

  • 实现和封装各种业务的Use Case,供外部调用。
  • 维护与业务相关的各种状态,例如是否正在编辑笔记。
  • Interactor可以获取各种Manager和Service,用于组合实现业务逻辑。这些Manager和Service应由外部注入依赖,而不是直接引用具体类。
  • 通过DataManager维护Model。
  • 监听各种外部业务事件并进行处理,并在必要时将事件发送给eventHandler。
  • Interactor持有一个由外部注入的eventHandler对象,可以将需要外部处理的业务事件发送给eventHandler,或者通过eventHandler接口对某些数据操作过程进行回调。
  • Interactor持有一个由外部注入的dataSource对象,用于获取View上的数据以更新Model。

Interactor是业务实现的践行者和维护者,它会调用各种Service来实现业务逻辑,并封装成明确的用例。这些Service在使用时也是基于接口的,因为Interactor的实现不与具体类绑定,而是由Application注入Interactor所需的Service。

示例代码:

@protocol ZIKNoteListInteractorProtocol <NSObject>
- (void)loadAllNotes;
- (NSInteger)noteCount;
- (NSString *)titleForNoteAtIndex:(NSUInteger)idx;
- (NSString *)contentForNoteAtIndex:(NSUInteger)idx;
- (NSString *)noteUUIDAtIndex:(NSUInteger)idx;
- (NSString *)noteTitleAtIndex:(NSUInteger)idx;
- (NSString *)noteContentAtIndex:(NSUInteger)idx;
@end

@interface ZIKNoteListInteractor : NSObject <ZIKNoteListInteractorProtocol>
@property (nonatomic, weak) id dataSource;
@property (nonatomic, weak) id eventHandler;
@end

@implementation ZIKNoteListInteractor

- (void)loadAllNotes {
    [[ZIKNoteDataManager sharedInsatnce] fetchAllNotesWithCompletion:^(NSArray *notes) {
        [self.eventHandler didFinishLoadAllNotes];
    }];
}

- (NSArray<ZIKNoteModel *> *)noteList {
    return [ZIKNoteDataManager sharedInsatnce].noteList;
}

- (NSInteger)noteCount {
    return self.noteList.count;
}

- (NSString *)titleForNoteAtIndex:(NSUInteger)idx {
    if (self.noteList.count - 1 < idx) {
        return nil;
    }
    return [[self.noteList objectAtIndex:idx] title];
}

- (NSString *)contentForNoteAtIndex:(NSUInteger)idx {
    if (self.noteList.count - 1 < idx) {
        return nil;
    }
    return [[self.noteList objectAtIndex:idx] content];
}

- (NSString *)noteUUIDAtIndex:(NSUInteger)idx {
    if (self.noteList.count - 1 < idx) {
        return nil;
    }
    return [[self.noteList objectAtIndex:idx] uuid];
}

- (NSString *)noteTitleAtIndex:(NSUInteger)idx {
    if (self.noteList.count - 1 < idx) {
        return nil;
    }
    return [[self.noteList objectAtIndex:idx] title];
}

- (NSString *)noteContentAtIndex:(NSUInteger)idx {
    if (self.noteList.count - 1 < idx) {
        return nil;
    }
    return [[self.noteList objectAtIndex:idx] content];
}

@end

Service

为Interactor提供各种封装好的服务,例如数据库访问、存储和定位功能等。Service在执行路由时由Application注入到Builder中,再由Builder注入到Interactor中。也可以只注入一个Service Router,这样在运行时通过这个Service Router来动态加载所需的Service,相当于注入了一个提供路由功能的Service。

Service可以被视为没有View的VIPER,它也有自己的路由和Builder。

Wireframe

翻译成中文为线框,用于表达从模块到另一个模块的过程。尽管它也担任路由角色的扮演者,但实际上它与Router有所不同。

Wireframe类似于Storyboard中已连接的一个个segue,负责提供一系列具体的路由用例。这些用例中已配置了源界面和目的界面的一些依赖,包括转场动画、模块间传参等。Wireframe的接口是供内部模块使用的,它通过调用Router来执行真正的路由操作。

示例代码:

@interface ZIKTNoteListWireframe : NSObject <ZIKTViperWireframe>
- (void)presentLoginViewWithMessage:(NSString *)message delegate:(id<ZIKTLoginViewDelegate>)delegate completion:(void (^ __nullable)(void))completion;
- (void)dismissLoginView:(UIViewController *)viewController animated:(BOOL)animated completion:(void (^ __nullable)(void))completion;
- (void)presentEditorForCreatingNewNoteWithDelegate:(id<ZIKTEditorDelegate>)delegate completion:(void (^ __nullable)(void))completion;
- (void)pushEditorViewForEditingNoteWithUUID:(NSString *)uuid title:(NSString *)title content:(NSString *)content delegate:(id<ZIKTEditorDelegate>)delegate;
- (UIViewController *)editorViewForEditingNoteWithUUID:(NSString *)uuid title:(NSString *)title content:(NSString *)content delegate:(id<ZIKTEditorDelegate>)delegate;
- (void)pushEditorViewController:(UIViewController *)destination fromViewController:(UIViewController *)source animated:(BOOL)animated;
- (void)quitEditorViewWithAnimated:(BOOL)animated;
@end

Router

Router是Application提供的具体路由技术,它可以简单封装UIKit中的跳转方法,也可以使用URL Router来执行路由。但一个模块不需要知道app使用了什么样的具体技术。Router才是真正连接各个模块的地方。它还负责查找对应的目的模块,并通过Builder进行依赖注入。

示例代码:

@interface ZIKTRouter : NSObject <ZIKTViperRouter>
///封装UIKit的跳转方法
+ (void)pushViewController:(UIViewController *)destination fromViewController:(UIViewController *)source animated:(BOOL)animated;
+ (void)popViewController:(UIViewController *)viewController animated:(BOOL)animated;
+ (void)presentViewController:(UIViewController *)viewControllerToPresent fromViewController:(UIViewController *)source animated:(BOOL)animated completion:(void (^ __nullable)(void))completion;
+ (void)dismissViewController:(UIViewController *)viewController animated:(BOOL)animated completion:(void (^ __nullable)(void))completion;
@end

@implementation ZIKTRouter (ZIKTEditor)

+ (UIViewController *)viewForCreatingNoteWithDelegate:(id<ZIKTEditorDelegate>)delegate {
    return [ZIKTEditorBuilder viewForCreatingNoteWithDelegate:delegate];
}

+ (UIViewController *)viewForEditingNoteWithUUID:(NSString *)uuid title:(NSString *)title content:(NSString *)content delegate:(id<ZIKTEditorDelegate>)delegate {
    return [ZIKTEditorBuilder viewForEditingNoteWithUUID:uuid title:title content:content delegate:delegate];
}

@end

Adapter

由Application实现,负责在模块通信时进行一些接口的转换,例如两个模块使用了相同业务功能的某个Service,使用的protocol实现一样,但protocol名称不同,就可以在路由时,在Adapter中进行一次转换。甚至只要定义的逻辑相同,依赖参数的名字和数据类型也可以允许不同。这样就能让模块不依赖于某个具体的protocol,而是依赖于protocol实际定义的依赖和接口。

注意这里的Adapter与Clean Architecture中的Interface Adapter是不同的。这里的Adapter就是字面意义上的接口转换,而Clean Architecture中的Interface Adapter则是更加抽象的一层,它是Use Case层与具体实现技术之间的转换,包含了更多的角色。

Builder

负责初始化整个模块,配置VIPER之间的关系,并声明模块所需的依赖,以便外部注入。

模块间解耦

一个VIPER模块可以被看作是一个独立的组件,可以单独被封装成一个库,并由app引用。这时,app负责将各个模块连接起来,也就是图中的灰色部分Application Context。一个模块肯定是存在于某个上下文环境中才能运行的。

Wireframe -> Router -> Adapter -> Builder 实现了模块间完整的路由,并实现了模块间的解耦。

其中Wireframe和Builder分别由引用者模块和被引用模块提供,是两个模块的出口和入口,而Router和Adapter则是由模块的使用者——Application实现的。

当两个模块之间存在引用关系时,意味着存在业务逻辑上的耦合,这种耦合是业务的一部分,是不可能消除的。我们能做的就是把耦合尽量交给模块调用者,由Application提供具体的类并注入到各个模块中,而模块内部仅面向protocol即可。这样,被引用模块只需实现相同的接口,就可以随时替换,甚至当接口有细微差异时,只要被引用模块提供了相同功能的接口,也可以通过Adapter进行接口兼容转换,以便让引用者模块无需进行任何修改。

Wireframe相当于插头,Builder相当于插座,而Router和Adapter相当于电路和转接头,将不同规格的插座和插头连接起来。将这些连接和适配工作交给Application层,就可以让两个模块实现独立。

子模块

大部分方案都没有讨论子模块的情况。在VIPER中如何引入另一个VIPER模块?多个模块之间如何交互?子模块由谁初始化、由谁管理?

在其他几个实现中,只有Uber详细讨论了子模块的问题。在Uber的Riblets架构中,子模块的Router被添加到父模块的Router中,模块之间通过delegate和监听进行通信。这样做会导致模块间产生一定的耦合。如果子模块是由于父View使用了一个子View控件而被引入的,那么父Interactor的代码里会多出一个子Interactor,这样就会导致View的实现方式影响了Interactor的实现。

子模块的来源

子模块的来源包括:

  • View引用了一个封装好的子View控件,连带引入了子View的整个VIPER。
  • Interactor使用了一个Service。

通信方式

子View可能是一个UIView,也可能是一个Child UIViewController。因此子View可能需要向外部请求数据,也可能独立完成所有任务,不需要依赖父模块。

如果子View可以独立,那么在子模块中不会出现与父模块交互的逻辑,只有通过输出接口传递一些事件。这时只需要将子View的接口封装在父View的接口中即可,父Presenter和父Interactor并不知道父View提供的这些接口是通过子View实现的。这样父模块就能接收到子模块的事件,并且能保持Interactor和Presenter、View之间从低到高的依赖关系。

如果父模块需要调用子模块的某些功能或从子模块获取数据,可以选择将这些操作封装在父View的接口中,但如果涉及到数据模型,并且不想让数据模型出现在View的接口中,可以将子Interactor作为父Interactor的一个Service,在引入子模块时,通过父Builder注入到父Interactor中,或者根据依赖关系进行彻底解耦,将其注入到父Presenter中,让父Presenter再将接口转发给父Interactor。这样子模块和父模块就可以通过Service的形式进行通信了,而这时,父Interactor也不知道这个Service来自子模块。

在这种设计下,子模块和父模块都不知道彼此的存在,仅仅是通过接口进行交互。好处是,如果父View想要更换为另一个相同功能的子View控件,只需要在父View中进行修改,不会影响Presenter和Interactor。

依赖注入

这个VIPER的设计通过接口将各个部分组合在一起,一个类需要配置很多依赖,例如Interactor需要依赖多个Service。这就涉及到两个问题:

  • 在哪里配置依赖
  • 一个类如何声明自己的依赖

在这个方案中,由Builder声明整个模块的依赖,然后在Builder内部为不同的类设置依赖,外部在注入依赖时,就不必知道内部如何使用这些依赖参数。如果一个类有必需的依赖参数,可以在init方法中直接体现,而对于非必需的依赖,可以通过暴露接口来声明。

如果需要动态注入,而不是在模块初始化时就配置所有依赖,Builder也可以提供动态注入的接口。

映射到MVC

如果你需要将一个模块从MVC重构到VIPER,可以按照以下步骤进行:

  • 整理Controller中的代码,将不同职责的代码用pragma mark分隔。
  • 整理好后,根据各部分的职责,将代码分散到VIPER的各个角色中,此时View、Presenter、Interactor之间可以直接互相引用。
  • 将View、Presenter、Interactor进行解耦,抽出接口,互相之间通过接口进行交互。

下面是第一步中可以在Controller中分隔的职责:

@implementation ViewController
//------View-------

//View的生命周期
#pragma mark View life

//View的配置,包括布局设置
#pragma mark View config

//更新View的接口
#pragma mark Update view

//View需要从model中获取的数据
#pragma mark Request view data source

//监控、接收View的事件
#pragma mark Send view event

//------Presenter-------

//处理View的事件
#pragma mark Handle view event

//界面跳转
#pragma mark Wireframe

//向View提供配置用的数据
#pragma mark Provide view data source

//提供生成model需要的数据
#pragma mark Provide model data source

//处理业务事件,调用业务用例
#pragma mark Handle business event

//------Interactor-------

//监控、接收业务事件
#pragma mark Send business event

//业务用例
#pragma mark Business use case

//获取生成model需要的数据
#pragma mark Request data for model

//维护model
#pragma mark Manage model

@end

这里缺少了View状态管理、业务状态管理等职责,因为这些状态通常都是@property,用pragma mark不能分隔它们,只能在@interface中声明时进行隔离。

方案二:允许适当耦合

上述方案旨在实现完全解耦,但在实际应用中,完全遵循此设计会导致代码量巨大。实际上,某些地方的耦合并不会引起太大问题,除非你的模块需要封装成通用组件以供多个应用使用。因此,我们将进一步简化方案,总结出哪些地方可以出现耦合,哪些耦合是不允许的。架构图如下:

final viper

View

  • View可以直接通过Router引入另一个子View,无需通过Presenter的路由来引入。
  • 如果View中的一些delegate变化的可能性不大,可以直接由Presenter实现(例如UITableViewDataSource),无需再次封装后交给Presenter。
  • View中不能出现Model类。

Presenter

  • Presenter可以直接调用Router执行路由,无需再通过Wireframe封装。
  • Presenter的接口参数中可以出现Model类,但不能导入Model类的头文件并直接使用Model类,只能用于参数传递。
  • Presenter中不建议导入UIKit,除非能保证不会使用那些会影响控件渲染的方法。

Interactor

  • 一些常用的Service可以直接引入,无需通过外部注入的方式使用。
  • Interactor可以用一个Service Router来动态获取Service。

路由和依赖注入

路由部分变化最大。View、Presenter和Interactor都可以通过路由获取某些模块。View可以通过路由获取子View,Presenter可以通过路由获取其他View模块,Interactor可以通过路由获取Service。

在实现时,可以将Wireframe、Router、Builder整合在一起,全部放到Router里,由模块实现并提供给外部使用。类似于Brigade团队和Rambler&Co团队的实现。但他们的实现是在Router中直接引入其他模块的Router,这样会导致依赖混乱,更好的方式是通过一个中间人提供其他模块的接口。

我在这里创建了一个轮子,通过protocol来寻找需要的模块并执行路由,无需直接导入目标模块中的类,并且提供了Adapter的支持,让多个protocol指向同一个模块。这样就能避免模块间的直接依赖。

示例代码:

///editor模块的依赖声明
@protocol NoteEditorProtocol <NSObject>
@property (nonatomic, weak) id<ZIKEditorDelegate> delegate;
- (void)constructForCreatingNewNote;
- (void)constructForEditingNote:(ZIKNoteModel *)note;
@end

@implementation ZIKNoteListViewPresenter

- (void)handleDidSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSAssert([[self.view routeSource] isKindOfClass:[UIViewController class]], nil);
    
    //跳转到编辑器界面;通过protocol获取对应的router类,再通过protocol注入依赖
    //App可以用Adapter把NoteEditorProtocol和真正的protocol进行匹配和转接
    [ZIKViewRouterForConfig(@protocol(NoteEditorProtocol))
	     performWithConfigure:^(ZIKViewRouteConfiguration<NoteEditorProtocol> *config) {
	         //路由配置
	         //跳转的源界面
	         config.source = [self.view routeSource];
	         //设置跳转方式,支持所有界面跳转类型
	         config.routeType = ZIKViewRouteTypePush;
	         //Router内部负责用获取到的参数初始化editor模块
	         config.delegate = self;
	         [config constructForEditingNote:[self.interactor noteAtIndex:indexPath.row]];
	         config.prepareForRoute = ^(id destination) {
	             //跳转前配置目的界面
	         };
	         config.routeCompletion = ^(id destination) {
	             //跳转结束处理
	         };
	         config.performerErrorHandler = ^(SEL routeAction, NSError * error) {
	             //跳转失败处理
	         };
	     }];
}

@end

总结

此方案依赖于一个统一的中间人,即路由工具。在我的实现中,就是ZIKRouter。View、Presenter、Interactor都可以使用对应功能的Router获取子模块。由于ZIKRouter仍然是通过protocol的方式与子模块交互,因此仍然维持模块间解耦。唯一的耦合就是各部分都引用了ZIKRouter这个工具。如果你想将模块与ZIKRouter的耦合也去除,可以让Router也变成面向接口,由外部注入。

Demo和代码模板

针对两个方案,同时编写了两个具有相同功能的Demo,可以比较一下代码上的区别。项目中同样提供了Xcode File Template用于快速生成VIPER代码模板。将.xctemplate后缀的文件夹复制到~/Library/Developer/Xcode/Templates/目录下,在Xcode的New->File->Template中即可选择代码模板快速生成代码。

参考