为什么要选择组件化
随着项目业务不断扩展,原来的单project
方式给我们的维护和扩展工作愈发困难,维护方面:我们如果需要去项目里找一个功能模块的viewController
除了搜索以外,似乎只能通过文件目录的逐层查找了;业务扩展方面:如果公司另外的APP需要引用已有项目的某个功能模块,这个时候我们似乎又只能采取copy
代码的方式了。这显然不是我们希望出现的工作方式。
显然组件化是化解上面困难点的优雅方式,通过一段时间的使用和阅读一些知名博客的总结,总结了组件化的一些优缺点:
优点:
- 简化了代码整体结构,降低了维护成本。
- 为代码和模块的复用提供了基础。
- 对不同的业务模块可以进行物理隔离(通过
git
仓库权限控制),进一步提升代码的稳定性和安全性。 - 极大地提升了整体架构的伸缩性,为将来的业务扩展打下了基础。
- 更好的分工协作,团队会安排团队成员各自维护一个相对独立的业务组件
- 单独的业务组件可以直接提交给测试使用
- 代码冲突的问题会变得很少
缺点
- 前期维护比较麻烦,对于前期功能尚未稳定的时候,修改、更新组件频率较高,比较费时。没有单
Project
的方便快捷。 - 入门门槛较高,新手入门需要的成本也更高
- 工具的使用成本,团队间和模块间的配合成本升高,开发效率短期会降低。
设计原则
组件化本身就是软件设计基本原则的体现,这里简要从几个方面阐述组件化怎样体现,怎么操作才能更好地遵循软件设计的原则。
高类聚
这里说的就是我们拆分组件粒度大小的原则了,什么样的功能模块才适合作为一个组件,如果一个功能模块只做一件或者一类事情,那么他就符合拆分成组件的基本要求了,即满足高类聚
的要求了。
低耦合
组件分层
合理划分组件之后,我们发现组件之间难免是会存在依赖关系的,我们如何减少组件之间的依赖呢?这里是我们先对组件进行分层。常见的分层结构为基础层
、核心层
、 业务层
,我们分层的目的是为了更好遵循依赖倒置原则,所谓依赖倒置原则为:
- 越底层的模块,应该越稳定,越抽象,越具有高复用度。
换言之就是我们基础层需要具有稳定性、可复用性,原因显而易见,只有稳定的基础设施才能孕育出健壮的程序。
底层组件不要依赖高层的组件,同一层的组件之间应该减少依赖。这里的分层是一个抽象的概念,需要开发者根据经验和实际需求对组件进行分层。
除了引入分层来划分组件之间的界线以外,我们会发现通常业务组件之间都存在或多或少的依赖关系,这里我们举例商城应用里,我们将商品详情和店铺详情各自抽离成一个组件,业务上难免会有相互依赖的时候,这个时候我们就得有一套组件之间通信的方案,常见的解决方案有target-action
和protocol
方案,这里我们选择的是target-action
,具体的实施方案会在下面通信主题里讲到。
关于category的使用
开放封闭原则
- 对扩展开放,对更改封闭。
- 类模块应该是可扩展的,但是不可修改。
不止是组件封装的时候,任何时候我们都应该遵循如此原则。根据这个原则,在面对新的需求时,我们应该尽可能地避免去更改底层组件结构,而选择去扩展。在开发中里我们一般怎么遵循此原则呢?
- 对于外界不关心的方法和属性不要暴露,对于部分属性尽可能对其读写属性进行描述。
- 尽可能对可能出现的业务更改留下扩展接口。比如当前需求让我们封装在一个加载文字的视图,这里我应该考虑到该视图在未来是否有可能会加载富文本或者加载图片。
通信
我在项目中使用的方案是target-action
,使用的工具是casa 大神的CTMediator
,CTMediator
的核心思路就是:通过target
找到对应的类,再通过action
找到对应的方法,在这里作为一个基础组件就可以使得业务组件之间的解耦。举个例子:
下面有两个组件:搜索列表组件和商品详情组件。
搜索列表点击列表上的商品会跳转到商品详情,于是搜索列表的组件就有以下代码:
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
UIViewController *viewController = [[CTMediator sharedInstance] performTarget:@"MJGoodsDetail" action:@"goodsDetail" params:@{
@"articleId":articleId} shouldCacheTarget:NO];
[self.navigationController pushViewController:viewController animated:YES];
}
在商品详情组件会去实现一个Target_MJGoodsDetail
的类和Action_goodsDetail
方法。
@implementation Target_MJGoodsDetail
- (id)Action_goodsDetail:(NSDictionary *)params {
NSString * articleId = [params objectForKey:@"articleId"];
MJGoodsDetailViewController *goodsDetailViewController = [[MJGoodsDetailViewController alloc] init];
goodsDetailViewController.articleId = articleId;
return goodsDetailViewController;
}
@end
更详细的介绍请到:https://github.com/casatwy/CTMediator。
在使用CTMediator
的过程中,需要考虑到路由文件的归宿问题,我在这里是将路由分为了面向组件和面向APP,所谓的面向组件的路由即组件自身将开放给外部使用的功能的标识,面向APP的路由即APP提供给外部(三方APP、H5)使用的功能的标识,面向组件的路由应由组件自身维护,而面向APP的路由则由主项目维护。所以常见的组件项目的目录结构为:
| -- Pods
| -- projectName
| -- API
| -- Router
| -- target_targetName.h
| -- target_targetName.m
| -- Controller
| -- View
| -- Model
组件拆分
一个项目可以完全在一个xcodeproj
里通过文件夹的方式来拆分组件,但是这种低效的方式显然不符合我们的要求,相信大部分iOS开发者都会使用cocoapods
来集成三方的库,深入了解cocoapods
之后会发现,这简直就是为组件化量身定制的,cocoapods
所做的工作一是完成组件自身的依赖管理,二是方便主项目管理组件。关于cocoapods
的使用可以查看:https://www.jianshu.com/p/069e91b9f8d5