前言:之前对于组件化的认知,仅停留于模块相互独立、分层的概念,另一方面,由于公司产品线较少,对于业务模块抽离以及模块间通信的方案没有明确的认知,这次就是需要全面学习了解一下。
1.组件化介绍1.1什么是组件化组件化就是将模块单独抽离、分层,并制定的方式,从而实现解耦,主要适用于大型团队开发项目。
这里的模块包含基础模块、功能模块、业务模块。
1.2组件化产生的原因有人说从来没用过组件化,也不影响项目开发。确实项目组件化不是项目开发的必要条件,但是项目实施组件化之后可以大大提高项目的开发效率,当项目越来越大的时候,维护的人员也不只是一两个人了,各个模块之间如果直接互相引用,就会产生许多耦合,当某个模块需要修改时,那么就需要修改依赖于这个模块的所有模块,想想这是不是一件很恐怖的事。
实施组件化,主要有4个原因:
模块间解耦
模块重用
提高团队协作开发效率
单元测试
对应的问题主要体现在:
修改某个模块的功能时,需要修改其他引用该模块的代码,这样会导致开发成本增加
模块对外接口不明确,外部甚至会调用不应暴露的私有接口,修改时耗费大量时间
修改代码时,涉及到其他的模块,容易影响其他成员的开发,产生代码冲突
当某个模块需要在其他产品线复用时,会发现耦合严重导致无法单独抽离
模块间的耦合导致接口和依赖混乱,难以编写单元测试
所以需要减少模块之间的耦合,用更规范的方式进行模块间交互。这就是组件化,也可以叫做模块化。
1.3实施组件化的前提上面有提到组件化并不是项目开发的必要条件,实施组件化是需要成本的,需要花费时间设计接口,分离代码,像以下这些情况就不需要组件化了,当然也需要结合实际情况进行考虑:
项目比较小,由于需求原因,模块间交互简单,耦合少
模块没有被多个外部模块引用,只是一个单独的小模块
模块不需要重用,代码几乎不会修改了
项目只有一两个人维护的时候
不需要编写单元测试
当有以下几个现象时,就需要考虑组件化了:
模块逻辑复杂,模块间耦合严重
项目规模变大,修改一个代码需要设计好几个地方
团队人数变多,经常代码冲突
项目编译耗时较大
模块的单元测试经常由于其他模块的修改而失败
1.4组件化方案的几条指标当我们需要组件化的时候,也需要设定一个目标,来标明组件化之后会带来什么样的效果,比如:
模块间没有直接耦合,一个模块内部的修改不会影响到另一个模块
模块可以单独被编译
模块间能够清晰的进行数据传递
模块可以被重用或者被另一个提供了相同功能的模块替换
模块的对外接口容易查找和维护
当模块的接口改变时,使用此模块的外部代码能够被高效的重构
尽量使用最少的修改和代码,让现有的项目实现模块化
支持OC和Swift,以及混编
前4条用于衡量一个模块是否被真正解耦,后面4条用于衡量在项目实践中的易用程度。
2.组件划分一般项目会分为基础组件、通用组件、业务组件三种,相应也划分成了不同的层级,当然,这里只是给个建议,具体的划分需要结合项目进行分析,如下图所示:
截屏-04-11下午3.15.39.png同时,需要注意的是:
只能上层对下层进行依赖
如果同一层组件之间有依赖,则将依赖部分提取出来,抽离为下一层的组件(依赖下沉)
3.组件间通信对于通用组件和基础组件,这两层很少会产生横向依赖,我们可以使用cocoapods把相应的代码封装成私有库,具体可见Cocoapods私有库的创建,这里就不做赘述了。
比较麻烦的是,或者称为业务模块,因为产品很多天马星空的想法,就让不同业务组件产生了相互依赖,这是不可避免的,没有耦合、没有依赖就无法形成一个项目,所以如何处理业务组件之间的依赖成为了组件化实施的重点。
有的项目中模块之间的关系如下图所示(图是随便画的,就是为了描述模块之间相互依赖的乱七八糟的关系):
截屏-04-11下午3.34.50.png从上图可以看到,每个模块都离不开其他模块,最终成了一坨,再改需求的时候,很容易形成连锁反应。
这样的一坨代码对于测试、编译、开发效率、后续扩展都有坏处,那怎么解决呢?在程序员的自我修养这本书中,看到过这样一句话:计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。这样理解的话,我们的问题瞬间逼格上升了,居然涉及到计算机系统软件体系结构了。
那我们就增加一个中间层,负责转发业务组件之间的信息,如下图所示:
截屏-04-11下午3.45.05.png现在看起来顺眼多了,中间层就是负责转发业务组件之间的信息,现在还会有几个问题:
中间层怎么去转发组件间调用?
一个模块只跟中间层通信,怎么知道另一个模块提供了什么接口?
上图中,模块和中间层之间相互依赖,怎么破除这个相互依赖?
3.1Target-action对于前两个问题,我们可以在中间层对外提供接口,实现时去调用对应模块的方法,如下:
//中间层#import"BookDetailComponent.h"#import"ReviewComponent.h"implementationMediator+(UIViewController*)BookDetailComponent_viewController:(NSString*)bookId{return[BookDetailComponentdetailViewController:bookId];}+(UIViewController*)ReviewComponent_viewController:(NSString*)bookIdreviewType:(NSInteger)type{return[ReviewComponentreviewViewController:bookIdtype:type];}end//BookDetailComponent组件#import"Mediator.h"#import"WRBookDetailViewController.h"implementationBookDetailComponent+(UIViewController*)detailViewController:(NSString*)bookId{WRBookDetailViewController*detailVC=[[WRBookDetailViewControlleralloc]initWithBookId:bookId];returndetailVC;}end//ReviewComponent组件#import"Mediator.h"#import"WRReviewViewController.h"implementationReviewComponent+(UIViewController*)reviewViewController:(NSString*)bookIdtype:(NSInteger)type{UIViewController*reviewVC=[[WRReviewViewControlleralloc]initWithBookId:bookIdtype:type];returnreviewVC;}end然后比如在阅读模块里这样使用:
//WRReadingViewController.m#import"Mediator.h"implementationWRReadingViewController-(void)gotoDetail:(NSString*)bookId{UIViewController*detailVC=[MediatorBookDetailComponent_viewControllerForDetail:bookId];[self.navigationControllerpushViewController:detailVC];UIViewController*reviewVC=[MediatorReviewComponent_viewController:bookIdtype:1];[self.navigationControllerpushViewController:reviewVC];}end这就是上面那个架构图的实现,这样看来依赖关系没有解除,中间层(Mediator)和模块之间仍然是相互依赖的关系。
对于OC来说有个办法可以解决这个问题,就是runtime反射调用:
//Mediator.mimplementationMediator+(UIViewController*)BookDetailComponent_viewController:(NSString*)bookId{Classcls=NSClassFromString("BookDetailComponent");return[clsperformSelector:NSSelectorFromString("detailViewController:")withObject:{"bookId":bookId}];}+(UIViewController*)ReviewComponent_viewController:(NSString*)bookIdtype:(NSInteger)type{Classcls=NSClassFromString("ReviewComponent");return[clsperformSelector:NSSelectorFromString("reviewViewController:")withObject:{"bookId":bookId,"type":(type)}];}end这下中间层(Mediator)没有再对组件有依赖了,也不需要#import什么东西了,对应的架构图就变成:
截屏-04-11下午3.59.08.png只有调用其他组件接口时才需要依赖,组件开发者不需要知道的存在,但是既然可以用就可以解耦取消依赖,那还用干啥?组件间调用时直接用接口调就行了,比如:
//WRReadingViewController.mimplementationWRReadingViewController-(void)gotoReview:(NSString*)bookId{Classcls=NSClassFromString("ReviewComponent");UIViewController*reviewVC=[clsperformSelector:NSSelectorFromString("reviewViewController:")withObject:{"bookId":bookId,"type":(1)}];[self.navigationControllerpushViewController:reviewVC];}end但是这样就会另外的问题:
写起来很恶心,代码提示都没有,每次调用写一坨
方法的参数个数和类型限制,导致只能每个接口都统一传一个NSDictionary。这个里的keyvalue是什么不明确,需要找个地方写文档说明和查看。
编译器层面不依赖其他组件,实际上还是依赖了,直接在这里调用,没有引入调用的组件时就挂了
所以需要将它移植到中间层后:
调用者写起来不恶心,代码提示也有了
参数类型和个数无限制,由去转就行了,组件提供的还是一个参数的接口,但在里可以提供任意类型和个数的参数,像上面的例子显式要求参数NSString*bookId和NSIntegertype
可以做统一处理,调用某个组件方法时如果某个组件不存在,可以做相应操作,让调用者与组件间没有耦合
到这里,基本上能解决我们的问题:各组件互不依赖,组件间调用只依赖中间件,不依赖其他组件。接下来就是优化这套写法,有两个优化点:
每一个方法里都要写方法,格式是确定的,这是可以抽取出来的
每个组件对外方法都要在写一遍,组件一多类的长度是恐怖的
优化后就成了casa的方案CTMediator,target-action对应第一点,target就是class,action就是selector,通过一些规则简化动态调用。Category对应第二点,每个组件写一个的,让不至于太长。
总结起来就是,组件通过中间层通信,中间层利用的、category特性动态获取模块,例如通过NSClassFromString获取类并创建实例,通过performSelector:+NSInvocation动态调用方法。
对于CTMediator的具体分析可以查看组件化方案学习-CTMediator这篇文章。
3.2URL路由这种方式是采用注册表的方式,用URL来表示接口,在模块启动时注册模块提供的接口,可以看下面这个简化的实现:
//Mediator.m中间件implementationMediatortypedefvoid(^