第一篇文章对VIPER进行了简单的介绍,这篇文章将从VIPER的源头开始,比较现有的几种VIPER实现,对VIPER进行进一步的职责剖析,并对各种细节实现问题进行挖掘和探讨。最后给出两个完整的VIPER实现,并且提供快速生成VIPER代码的模板。
Demo和轮子的github地址是:ZIKViper,路由工具:ZIKRouter。有用请点个star~
两个实现展示了以下问题的解决方案:
- 如何彻底地解决不同模块之间的耦合
- 如何在一个模块里引入子模块
- 子模块和父模块之间如何通信
- 如何对模块进行依赖注入
- 面向接口的路由工具
起源
VIPER架构,最初是2013年在MutualMobile的技术博客上,由Jeff Gilbert 和 Conrad Stoll 提出的。他们的博客网站有过一次迁移,原文地址已经失效,这是迁移后的博文:MEET VIPER: MUTUAL MOBILE’S APPLICATION OF CLEAN ARCHITECTURE FOR IOS APPS。
这是文章中提出的架构示意图:
Wireframe可以看作是Router的另一种表达。可以看到,VIPER之间的关系已经很明确了。之后,作者在2014年在objc.io上发表了另一篇更详细的介绍文章:Architecting iOS Apps with VIPER。
在作者的第一篇文章里,阐述了VIPER是在接触到了Uncle Bob的Clean Architecture后,对Clean Architecture的一次实践。因此,VIPER真正的源头应该是Clean Architecture。
Clean Architecture
由Uncle Bob在2011年提出的Clean Architecture
,是一个平台无关的抽象架构。想要详细学习的,可以阅读作者的原文:Clean Architecture,翻译:干净的架构The 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 -> Entity
,Wireframe
严格来说也是一类特殊的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的优点总结了一下,提出了这样的架构图:
他们对VIPER的各部分都没有异议,只是对Interactor的实现进行了进一步细化。用一个Data Manager提供给各个Use Case管理Entity,比如获取、存储功能。在Service中调用网络层去获取服务端的数据。
文章中还认为应该由Wireframe负责初始化整个VIPER,生成各部分的类,并设置依赖关系,并且引用另一个模块的Wireframe,负责跳转到另一个界面。
和这个实现类似的还有:
- VIPER to be or not to be?。这篇文章里给出了一个Unit Test的例子,还探讨了应该在什么情况下开始应用VIPER。
- VIPER architecture: Our best practices to build an app like a boss。Demo地址:Boilerplate_iOS_VIPER。
针对VIPER需要编写太多初始化代码的麻烦,可以使用Xcode自带的Template解决。而很多作者都提到了一个代码生成工具:Generamba。
争议
文章并没有对VIPER进行修改,只是进一步细化了。这应该是一个最简单的实现。如果你要实施VIPER,参照这篇文章来没有什么大问题。但是它没有探讨的问题是:
- 如何解决不同Wrieframe之间的耦合?
- Wrieframe如何知道其他模块需要的初始化参数?
- 在模块间通信时,Interactor的数据如何传递给另一个模块?
- 父模块和子模块之间是怎样的关系?
Rambler&Co团队的实现
一个对VIPER十分感兴趣的俄国团队,编写了一本关于VIPER的书:The-Book-of-VIPER。并且给出了一个目前网络上实现完成度最高的开源Demo:rambler-it-ios,以及他们用于实施VIPER的库:ViperMcFlurry。
他们整理的VIPER架构图如下:
和其他实现不同的是,他们把VIPER的初始化和装配工作单独放到了一个Assembly里,Router只做界面跳转的工作。并且把VIPER内不同部分之间的通信统一用Input和Output来表示。Input表示外部主动调用模块提供的接口,Output表示模块通过外部实现所要求的接口,将事件传递到外部。
之所以将模块初始化单独放到Assembly里,是因为Router如果负责初始化本模块,会违背单一职责原则。