iOS VIPER架构实践(二):VIPER详解与实现

第一篇文章对VIPER进行了简单的介绍,这篇文章将从VIPER的源头开始,比较现有的几种VIPER实现,对VIPER进行进一步的职责剖析,并对各类细节实现问题进行挖掘和探讨。最后给出两个完整的VIPER实现,而且提供快速生成VIPER代码的模板。html

Demo和轮子的github地址是:ZIKViper,路由工具:ZIKRouter。有用请点个star~ 注意,Demo须要先用pod install安装一下依赖库。ios

两个实现展现了如下问题的解决方案:git

  • 如何完全地解决不一样模块之间的耦合
  • 如何在一个模块里引入子模块
  • 子模块和父模块之间如何通讯
  • 如何对模块进行依赖注入
  • 面向接口的路由工具

目录

  • 起源
  • Clean Architecture
    • Enterprise Business Rules
    • Application Business Rules
    • Interface Adapters
    • Frameworks & Drivers
    • 总结
  • 现有的各类VIPER实现
    • Brigade团队的实现
      • 争议
    • Rambler&Co团队的实现
      • 争议
    • Uber团队的实现
      • 各部分职责
      • 数据驱动
      • 争议
      • 其余设计
  • 方案一:最完整的VIPER
    • View
    • Presenter
    • Interactor
    • Service
    • Wireframe
    • Router
    • Adapter
    • Builder
  • 模块间解耦
  • 子模块
    • 子模块的来源
    • 通讯方式
  • 依赖注入
  • 映射到MVC
  • 方案二:容许适当耦合
    • View
    • Presenter
    • Interactor
    • 路由和依赖注入
    • 总结
  • Demo和代码模板
  • 参考

起源

VIPER架构,最初是2013年在MutualMobile的技术博客上,由Jeff Gilbert 和 Conrad Stoll 提出的。他们的博客网站有过一次迁移,原文地址已经失效,这是迁移后的博文:MEET VIPER: MUTUAL MOBILE’S APPLICATION OF CLEAN ARCHITECTURE FOR IOS APPSgithub

这是文章中提出的架构示意图:数据库

viper-mutualmobile

Wireframe能够看做是Router的另外一种表达。能够看到,VIPER之间的关系已经很明确了。以后,做者在2014年在objc.io上发表了另外一篇更详细的介绍文章:Architecting iOS Apps with VIPERswift

在做者的第一篇文章里,阐述了VIPER是在接触到了Uncle Bob的Clean Architecture后,对Clean Architecture的一次实践。所以,VIPER真正的源头应该是Clean Architecture。后端

Clean Architecture

由Uncle Bob在2011年提出的Clean Architecture,是一个平台无关的抽象架构。想要详细学习的,能够阅读做者的原文:Clean Architecture,翻译:干净的架构The Clean Architecturebash

它经过梳理软件中不一样层之间的依赖关系,提出了一个自外向内,单向依赖的架构,以下图所示:网络

Clean Architecture

越靠近内层,越变得抽象,越接近设计的核心。越靠近外层,越和具体的平台和实现技术相关。内层的部分彻底不知道外层的存在和实现方式,代码只能从外层向内层引用,目的是为了实现层与层之间的隔离。将不一样抽象程度的层进行隔离,作到了把业务规则和具体实现分离开。你能够把外层看做是内层的delegate,外层只能经过内层提供的delegate接口来使用内层。数据结构

Enterprise Business Rules

表明了这个软件项目的业务规则。由数据实体体现,是一些能够在不一样的程序应用之间共享的数据结构。

Application Business Rules

表明了本应用所使用的一些业务规则。封装和实现了用到的业务功能,会将各类实体的数据结构转为在用例中传递的实体类,可是和具体的数据库技术或者UI无关。

Interface Adapters

接口适配层。将用例的规则和具体的实现技术进行抽象地对接,将用例中用到的实体类转为供数据库存储的格式或者供View展现的格式。相似于MVVM中把Model的数据传递给ViewModel供View显示。

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

Frameworks & Drivers

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

总结

能够看到,Clean Architecture里已经出现了Use Case、Interactor、Presenter等概念,它为VIPER的工程实现提供了设计思想,VIPER将它的设计转化成了具体的实现。VIPER里的各部分正是存在着由外向内的依赖,从外向内表现为:View -> Presenter -> Interactor -> EntityWireframe严格来讲也是一类特殊的Use Case,用于不一样模块之间通讯,链接了不一样的Presenter

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

现有的各类VIPER实现

MutualMobile的那两篇文章虽然已经明确了VIPER各部分之间的职责,而且给出了简单的Demo,可是对Wireframe部分的实现有些争议,解耦作得不够完全,而且对各层之间如何交互还处在最简单的实现上。以后出现了挺多文章来将VIPER进一步细化,不过某些细节的实现上有些差异,在给出我本身的VIPER以前,我将先对这些实现进行一次综合的比较分析,看看他们都使用了哪些技术,遇到了哪些争议点。不一样实现之间已经公认的地方我就再也不单独列出了。

Brigade团队的实现

原文地址:Brigade’s Experience Using an MVC Alternative: VIPER architecture for iOS applications

文章把VIPER的优势总结了一下,提出了这样的架构图:

Brigade’s VIPER

他们对VIPER的各部分都没有异议,只是对Interactor的实现进行了进一步细化。用一个Data Manager提供给各个Use Case管理Entity,好比获取、存储功能。在Service中调用网络层去获取服务端的数据。

文章中还认为应该由Wireframe负责初始化整个VIPER,生成各部分的类,并设置依赖关系,而且引用另外一个模块的Wireframe,负责跳转到另外一个界面。

和这个实现相似的还有:

针对VIPER须要编写太多初始化代码的麻烦,可使用Xcode自带的Template解决。而不少做者都提到了一个代码生成工具:Generamba

争议

文章并无对VIPER进行修改,只是进一步细化了。这应该是一个最简单的实现。若是你要实施VIPER,参照这篇文章来没有什么大问题。可是它没有探讨的问题是:

  • 如何解决不一样Wrieframe之间的耦合?
  • Wrieframe如何知道其余模块须要的初始化参数?
  • 在模块间通讯时,Interactor的数据如何传递给另外一个模块?
  • 父模块和子模块之间是怎样的关系?

Rambler&Co团队的实现

一个对VIPER十分感兴趣的俄国团队,编写了一本关于VIPER的书:The-Book-of-VIPER。而且给出了一个目前网络上实现完成度最高的开源Demo:rambler-it-ios,以及他们用于实施VIPER的库:ViperMcFlurry

他们整理的VIPER架构图以下:

Rambler&Co's VIPER

和其余实现不一样的是,他们把VIPER的初始化和装配工做单独放到了一个Assembly里,Router只作界面跳转的工做。而且把VIPER内不一样部分之间的通讯统一用Input和Output来表示。Input表示外部主动调用模块提供的接口,Output表示模块经过外部实现所要求的接口,将事件传递到外部。

之因此将模块初始化单独放到Assembly里,是由于Router若是负责初始化本模块,会违背单一职责原则。

争议

这个实现的愿景很好,只是在转变为具体实现的时候不够完美,有不少问题尚待解决。具体能够参见Demo。

  • Assembly使用了Typhoon这个依赖注入工具,经过Method Swizzling自动初始化VIPER的各个部分

我对Typhoon这个依赖注入工具不是特别感冒,它使用了十分复杂的run time技术,想要追踪一个对象的注入过程时,会看得晕头转向。并且它没法实现运行时由调用方动态注入,只能实现预约义好的静态注入。也就是不能动态传参。

  • 使用storyboard进行路由

在Demo中实现了在执行segue时用block来使用-prepareForSegue:sender:,实现向目的界面传参,实现了动态注入。可是这样就把路由限定在了storyboard的segue技术上,那么对于那些没有使用storyboard的项目应该怎么办呢?Demo并无给出答案。并且-prepareForSegue:sender:只能向View传参,可是有一些参数是View不该该接触到的,而是应该直接传给Presenter或者Interactor的。

  • 有时候模块须要从Output中获取数据,例如Presenter主动获取View中的文字,传递给Interactor,此时Output并不能完整描述它的职责,还能够再进一步划分

也就是说,他们的方案在设计上是不错的,但在技术上还有不少改进空间。

Uber团队的实现

Uber因为业务愈来愈复杂,旧项目的架构已经没法知足当前的需求,所以在2016年彻底重构了他们的 rider app。他们借鉴VIPER,而且设计出了一个VIPER的变种架构:Riblets。文章地址:ENGINEERING THE ARCHITECTURE BEHIND UBER’S NEW RIDER APP

架构图以下:

riblets

数据流向图:

数据流向

父模块和子模块之间通讯:

父子模块间通讯

各部分职责

这里只列出一些和VIPER有差别的地方:

  • Builder负责初始化Riblets模块内的各个部分,定义了模块的依赖参数
  • Component负责获取和初始化那些不是Riblets模块内的部分,例如services,并注入到Interactor中
  • Router负责管理子模块,持有子模块的Router,并把子模块的View添加到视图树上
  • Interactor经过调用Service管理Model,而不是在Interactor中直接管理
  • Interactor和子模块的Interactor经过监听者模式和delegate互相通讯

数据驱动

最大的改变是将Router从Presenter移到了Interactor,改变了模块的主从关系,整个模块的生命周期如今由Interactor来管理。而以前的VIPER模块是依赖于View的生命周期的。这样一来,整个架构就从View驱动变成了业务驱动,或者数据驱动。

关于这个改变,Uber给出了两个缘由:

  • 想要统一iOS和Andorid的软件架构,以及更好地互相借鉴开发经验和教训,于是须要改变iOS中视图驱动的设计
  • 想要建立一个没有View,只有业务逻辑的模块,所以生命周期须要由Interactor管理

争议

Uber团队的确颇有想法。在对他们的这个方案进行深刻实践以前,我没法评论这个方案是好是坏,我只在这里提出一些实践中可能会遇到的问题。

关于Uber给出的第一个缘由,这是Uber团队基于协调两个开发团队的状况而作出的选择,若是咱们没有他们这样统一开发的需求,并无必要借鉴。iOS的UIKit是一个视图驱动的框架,很难作到100%数据驱动,在实践中将会遇到许多须要解决的问题,除非有足够的开发时间,不然不要草率地投入其中。是否要使用数据驱动的设计,仍是应该由项目的业务设计来决定。当数据变化大部分是由后端的Service和网络数据引发时,再去考虑数据驱动吧。例如Uber的地图路线由定位模块不断计算,自动更新,就比较适合使用数据驱动。

关于第二个缘由,一个没有View和Presenter的VIPER,就只剩下Router、Interactor、Model,这时这个模块能够看作是一个能够经过Router调用的Service或者Manager,这个Service有本身的状态和生命周期,Service也能够在View销毁后继续完成剩余的业务工做,只要业务须要,能够进行自持有,自释放。并且这个Service最终仍是会表如今某个View上。这么看来,Router的层级已经升高了,成为了整个app内的模块间通讯工具,能够链接任意模块,不只仅是VIPER,所以Router由谁持有,就彻底由模块内部自由管理了。

只是,在iOS中的VIPER里,实际的路由API都是存在于UIViewController上的,Router会直接和View产生引用,把Router放到和View隔离的Interactor里会破坏隔离。并且从Clean架构的分层来看,层级升高后的Router应该是处在Interface Adapter层和Framework & Driver层之间,而Interactor则是在Application Business Rules层,由Interactor来管理其余角色,会破坏了Clean Architecture里的依赖关系。

好比一个没有View的、用于管理语音通话数据的Interactor,收到了通话异常中断的事件,在处理事件时,它不该该经过Router将本身移除,或者结束整个语音通话业务,或者自动调用从新拨号的业务,这样很容易会让不一样的Use Case之间产生耦合,这些都应该由更上层的Service去选择执行,若是有页面跳转的设计,则应该把事件转发给一个存在Presenter层的Parent VIPER模块,由parent来决定是退出通话界面仍是弹窗提示。当一个Interactor没有Presenter和View时,它必定是另外一个VIPER的子模块。这么看来,在没有View时,或许让Service来持有Router才是正确的。

所以,若是真的有把VIPER变成数据驱动的需求,主要仍是源于Uber给出的第一个基于团队统一的理由。

其余设计

文章里还给出了一些颇有参考价值的内容,好比:

  • 对Interactor进行注入的Component
  • 视图树变成了Router树
  • Interactor不直接维护Model,而是经过对应的Service来维护Model
  • 父模块和子模块之间经过Interactor来通讯

Uber的这个方案讲了不少其余方案没有提到的方面,好比依赖注入、如何引入子模块等问题。不过这个方案并无开源。

方案一:最完整的VIPER

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

thorough viper

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

View

View能够是一个UIView + UIViewController,也能够只是一个custom UIView,也能够是一个自定义的用于管理UIView的Manager,只要它实现了View的接口就能够。

View层的职责:

  • 展现界面,组合各类UIView,并在UIViewController内管理各类控件的布局、更新
  • View对外暴露各类用于更新UI的接口,而本身不主动更新UI
  • View持有一个由外部注入的eventHandler对象,将View层的事件发送给eventHandler
  • View持有一个由外部注入的viewDataSource对象,在View的渲染过程当中,会从viewDataSource获取一些用于展现的数据,viewDataSource的接口命名应该尽可能和具体业务无关
  • View向Presenter提供routeSource,也就是用于界面跳转的源界面

View层会引入各类自定义控件,这些控件有许多delegate,都在View层实现,统一包装后,再交给Presenter层实现。由于Presenter层并不知道View的实现细节,所以也就不知道这些控件的接口,Presenter层只知道View层统一暴露出来的接口。并且这些控件的接口在定义时可能会将数据获取、事件回调、控件渲染接口混杂起来,最具表明性的就是UITableViewDataSource里的-tableView:cellForRowAtIndexPath:。这个接口同时涉及到了UITableViewCell和渲染cell所须要的Model,是很是容易产生耦合的地方,所以须要作一次分解。应该在View的dataSource里定义一个从外部获取所须要的简单类型数据的方法,在-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
复制代码

通常来讲,viewDataSource和eventHandler都是由Presenter来担任的,Presenter接收到dataSource请求时,从Interactor里获取并返回对应的数据。你也能够选择在View和Presenter之间用ViewModel来进行交互。

Presenter

Presenter由View持有,它的职责有:

  • 接收并处理来自View的事件
  • 维护和View相关的各类状态和配置,好比界面是否使用夜间模式等
  • 调用Interactor提供的Use Case执行业务逻辑
  • 向Interactor提供View中的数据,让Interactor生成须要的Model
  • 接收并处理来自Interactor的业务事件回调事件
  • 通知View进行更新操做
  • 经过Wireframe跳转到其余View

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

示例代码:

@interface ZIKNoteListViewPresenter () <ZIKNoteListViewDataSource, ZIKNoteListViewEventHandler>
@property (nonatomic, strong) id<ZIKNoteListWireframeProtocol> wireframe;
@property (nonatomic, weak) id<ZIKViperView,ZIKNoteListViewProtocol> view;
@property (nonatomic, strong) id<ZIKNoteListInteractorInput> 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 ZIKNoteListInteractorInput <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 <ZIKNoteListInteractorInput>
@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里,再由Buidler注入到Interactor里。也能够只注入一个Service Router,在运行时再经过这个Service Router懒加载须要的Service,至关于注入了一个提供Router功能的Service。

Service能够看做是没有View的VIPER,也有本身的路由和Builder。

Wireframe

翻译成中文叫线框,用于表达从一个Module到另外一个Module的过程。虽然也是扮演者执行路由的角色,可是其实它和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才是真正链接各个模块的地方。它也负责寻找对应的目的模块,而且经过Buidler进行依赖注入。

示例代码:

@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能够独立,那在子模块里不会出现和父模块交互的逻辑,只有把一些事件经过Output传递出去的接口。这时只须要把子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里声明的时候进行隔离。

方案二:容许适当耦合

上面的方案是以最完全的解耦为目标设计的,在实践中,若是真的彻底按照这个设计,代码量的确不小。其实一些地方的耦合并不会引发多大问题,除非你的模块须要封装成通用组件供多个app使用,不然并不须要按照100%的解耦要求来编写。所以接下来我再总结一个稍微简化的方案,总结一下各部分能够在哪些地方出现耦合,哪些耦合不能出现。

在这个方案里,我使用了一个中介者来减小一部分代码,Router就是一个很适合成为中介者的角色。

架构图以下:

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

  • 一些app中经常使用的Service能够直接引入,不须要经过外部注入的方式来使用
  • Interactor能够用一个Service Router来动态获取Service

路由和依赖注入

改变得最多的就是路由部分。View、Presenter和Interactor均可以使用路由来获取一些模块。View能够经过路由获取子View,Presenter能够经过路由获取其余View模块,Interactor能够经过路由获取Service。

在实现时,能够把Wireframe、Router、Builder整合到一块儿,全都放到Router里,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进行匹配和转接
    [ZIKViewRouterToModule(NoteEditorProtocol)
	     performFromSource:[self.view routeSource] //跳转的源界面
	     configuring:^(ZIKViewRouteConfiguration<NoteEditorProtocol> *config) {
	         //路由配置
	         //设置跳转方式,支持全部界面跳转类型
	         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,能够比较一下代码上的区别。地址在:ZIKViper。注意,Demo须要先用pod install安装一下依赖库。

项目里也提供了Xcode File Template用于快速生成VIPER代码模板。把.xctemplate后缀的文件夹拷贝到~/Library/Developer/Xcode/Templates/目录下,就能够在Xcode的New->File->Template里选择代码模板快速生成代码。

参考

相关文章
相关标签/搜索