一、为什么组件化
1、模块间解耦
每个模块尽可能可以进行独立开发、调试、测试、打包等,模块内部改动不影响或尽量少影响主APP和其他模块;
2、模块重用
模块解耦实际上也是为了更好的复用。一个模块可能会被多个模块或者其他app所使用的,那么组件化把模块独立出来,方便复用。
3、提高团队协作开发效率
组件化使得团队成员分工更加明确,各自负责各自的业务线。每个人只需关注自身组件业务开发,不用关心其他组件的具体实现细节。自身改动也不用担心影响其他模块。也避免了各个业务代码频繁提交合并等操作以及可能带来的问题。
4、单元测试
缩小测试范围,有利于问题定位和解决。排除其他模块可能带来的影响。
注意:哪些项目不适合组件化
1、项目较小,模块间交互简单,耦合少
2、模块没有被多个外部模块引用,只是一个单独的小模块
3、模块不需要重用,代码也很少被修改
4、团队规模很小
二、如何组件化
1、首先进行分层
客户端APP主流框架都将应用分成三层:
这三层主要有业务层、通用层和基础层。
- 业务层。就是我们APP具体的业务。比如常见的首页、发现、我的等模块。
- 通用层。比如常用控件、数据管理、分享、三方登录等等。
- 基础层。比如网络、数据库等。
分层的时候需要注意一下几个原则:
- 只能上层对下层依赖
比如业务层可以依赖通用层和基础层,通用层也可以依赖基础层,但是通用层和基础层不能依赖业务层,基础层也不能依赖通用层。 - 公共代码下沉
当我们遇到同层多个模块都会引用的代码时,可以考虑代码下沉,封装成一个通用业务模块,使得代码可以被复用,提高效率。 - 减少横向依赖、适当下沉
同层间相互依赖可以适当增加中间件(Mediator)进行解耦,这样中间件下沉,减少同层间直接依赖。
2、模块划分
模块的划分是一个比较难的点。除了要考虑业务本身,还要考虑到团队人员情况。要怎么划分、划分的粒度有多大要具体情况具体分析。
但是单从技术的角度考虑,模块划分应该从基础层开始划分。为什么呢?
- 首先,最上层通常是业务层,如果先从上层开始划分会直接影响到具体业务,而且组件划分不是一朝一夕能完成,中间肯定会影响业务的开发;
- 其次,按照大家的正常思维,上层依赖下层,上层的代码依赖的代码比较多,如果从上层开始,就会出现减代码的情况,会存在很多不可预估的风险;
- 再次,从基础层开始的话,因为基础层不涉及到具体业务,也不影响业务开发。基础层可以再开发、测试完成确定稳定可靠之后再让上层代码慢慢接入,这时候上层代码在划分的时候遇到对基础的依赖就可以直接替换成新的基础模块,平滑进行过度。
对于通用业务层的划分,如果是比较确定的可以提前划分好。有些模块可能需要在业务层具体划分的时候视情况增加。业务层的划分就得具体情况具体分析了。模块划分时遇到交叉依赖问题,适当增加中间件解耦(Mediator),常用的解耦方式有:路由、target-action (CDMediator, invocation)和protocol classes。具体可以参考下面的常见组件化方案的比较。
常见组件化方案的比较
可以直接参考网友的一篇文章iOS组件化方案对比;
通过Cocoa Pod来管理组件
组件拆分之后通过Pod来管理,Pod创建私有库参考使用Cocoapods开发SDK;
通过代理方式实现组件化方案原理
这里的组件化方案只针对业务层和主app之间以及业务层和业务层之间组件化的方案。因为对于基础成和通用业务层,他们都属于下层代码,功能相对单一,层间依赖较少甚至,比较容易划分,而且相对比较稳定,业务层是可以通过各自的中间层或者直接依赖的。
代理方案实现组件化的三个要素
核心实现由三大部分组成:组件管理模块、协议模块和和各个业务组件模块。主APP和组件以及组件间的通信通过代理来实现。首先组件管理模块负责组件和协议的映射管理;协议模块声明了各个模块提供给外部访问的代理方法;各个组件有对应的代理实现了对应协议。每个组件都要依赖组件管理模块和协议模块,动态将各自模块的协议和负责实现协议代理对象的类名注册到组件管理模块的映射表中,同时可以根据需要可以通过协议模块中其他组件的代理方法与其他组件进行通信。
各组件的协议和实现协议的代理对象是任意的吗?
为了便于管理,每个组件对外的协议都有一个专门的中介层来实现这个协议,这个中介层负责组件内部和组件外部通信。这个中介层通常是一个对象,当可能存在多个协议时,可以通过分类等方式进行模块化。这样做的目的是为了减少组件间通信的复杂度,保持内部对外通信的统一性。外部访问这个组件时只需要找到这个组件对应的一个协议就可以了,而内部只需要有一个专门的中介层来接收外部信息,并统一在组件内部进行调度,使得整个通信流程有法可依、有迹可循。
组件的协议和其实现类是在什么时机注册到映射表中的呢?
- 第一种方法可以在代理类的+load方法中动态注册,直接注册代理对象和协议的映射关系,但是这样做的代码很多的话会影响启动速度。至于为什么会影响启动速度,这个问题涉及到类的加载流程,具体参考OC类的加载流程;
@implementation LNFeedModule
+(void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[[LNModuleManager sharedInstance] addImpClassName:@"LNFeedModule" protocolName:@"LNFeedModuleProtocol"];
});
}
实现+load方法会导致LNFeedModule称为非懒加载类,在程序启动是加载,消耗性能。
- 第二种方法是在C++构造方法中注册, 这样做虽然也会导致LNModuleManager被提前加载,但是LNModuleManager只会被加载一次,影响不大。而且本社注册类名和协议名称只不过是简单的字典操作,耗时较少,对启动性能影响不大,最重要的是实现简单。每个模块实现自己的C++构造方法即可。
//注册类名和协议名称
__attribute__((constructor)) void addModulLNFeedModule(void){
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[[LNModuleManager sharedInstance] addImpClassName:@"LNFeedModule" protocolName:@"LNFeedModuleProtocol"];
});
}
- 第三种方法可以参考阿里的BeeHive组件化工具。利用注解在程序编译时把对应的协议的实现和协议写入mach-o文件中,待镜像文件加载时读取,性能较好。实现虽然复杂,但是也已经封装好了,可以直接用。
每个组件如何监听APP的生命周期?
每个组件对应的协议都会继承UIAppDelegate协议,这样每个组件都可以根据需要监听应用程序的代理方法,完成自身相关的操作。
总结
架构设计的最终目的是为了提高效率。抛开开发人员自身的问题,这个效率主要有几个方面影响:其一、程序架构是否简单易懂,越简单易懂,自然越容易保障开发效率;其二、尽可能少写代码,代码复用率高,复用率高一定程度上能提高开发效率;其三、专注于自身业务,尽量的少关注其他人。组件化不要只是为了解耦而解耦,而是一切以是否有利于提高效率为目的。解耦应该是为了可扩展性、可维护性和可复用性等,但同时要兼顾可读性和可靠性。
网友评论