最近在学习组件化的一些方案。这里收集消化了一下,分享给大家参考。
组件化是架构层面的一个概念,它把项目按照某些规则(比如:按功能、按业务)划分成若干个颗粒度较小的单位,我们把这些单位称之为组件,或者是模块,来达到优化项目结构的目的。
组件又可以细分为 功能组件(如:图片库,网络库),业务组件也叫模块(如:订单模块,个人中心模块)
功能组件主要是物理层面的拆分,方便以后的复用
业务组件强调逻辑拆分,以便解耦
组件化的发展历程
开发之初,在功能方面,我们会把项目划分为基础层、网络层、数据层等等,而业务层面,仅仅是按照目录结构做一个简单的模块分层,比如订单模块、个人中心模块
注:因为功能组件被大部分业务模块所依赖,这里暂时不讨论功能模块的架构问题。
随着业务的发展,项目变的越来越复杂,APP
内各个业务之间的耦合严重,边界越来越模糊,经常会出现你中有我,我中有你的情况,如图
可以看到,模块与模块严重耦合,对代码的扩展,以及代码的开发效率造成了巨大影响,有一种改一处动全身的感觉。发展到这个阶段,我们会把各个模块分割开来,通过中介者来完成不同模块之间的交互。如图
这个时候,架构看起来是清晰了许多。但因为中介者任然反向依赖业务模块,任然存在改一处多个模块受影响的情况,依赖仍旧是双向的。我们举个例子:
假如我现在在会员模块,要跳入到商品模块,此时,会员模块需要通过中介者来完成跳转
// 会员模块
[self.Intermediary gotoGoodsModuleWithParam:param]
// 在中介者中
- (void)gotoGoodsModuleWithParam:(id)param {
self.goodsModule.param = param; // 参数
[self.memberModule.navigationController pushViewController:self.goodsModule]
}
假如此时我们的需求变了,除了传递参数外,还需要额外传一个参数来决定导航条的隐藏或者背景色,此时我们修改gotoGoodsModuleWithParam
函数的结构,那么就需要修改两个地方,除了修改中介者中的方法以外,还需要修改模块中使用该函数的地方。
互相依赖其实是开发时候的大忌,我们还要考虑模块的循环引用问题、开发效率的问题、复用问题等等
如果消除中介者对模块的依赖呢?如图:
是不是有那味道了。各模块互不干涉,明确职责和边界。
那么如何实现这种功能呢?下面来看看业内的一些技术方案。
业内组件化方案
- 基于路由URL的UI页面统跳管理
- 基于反射的接口调用封装
- 基于面向协议思想的服务注册方案
- 基于通知的广播方案
基于路由URL的UI页面统跳管理
一般用法
// kRouteGoodsDetail = @"/goods/goods_detail"
UIViewController * vc = [Router handleURL:kRouteGoodsDetail];
if (vc) [self.navigationController pushViewController:vc animated:YES];
传参的情况
//kRouteGoodsDetails = @“//goods/goods_detail?goods_id=%d”
NSString *urlStr = [NSString stringWithFormat:kRouteGoodsDetails, 123];
UIViewController *vc = [Router handleURL:urlStr];
if(vc) {
[self.navigationController pushViewController:vc animated:YES];
}
复杂的参数类型
+ (nullable id)handleURL:(nonnull NSString *)urlStr
complexParams:(nullable NSDictionary*)complexParams
completion:(nullable RouteCompletion)completion;
从接口上可以看出来,通过@"/goods/goods_detail"
字符串,我们可以拿到需要交互的模块,来进行操作。大家可以参考Bifrost,有一定知识储备的话,看起来会比较容易。当然这只是其中一种思路。
在Demo
中,作者把各个模块以子项目的形式汇聚在一个主工程里,在需要路由服务的页面,通过bindURL
的形式,完成字符串
与路由
的绑定。
但是:路由的绑定放到load
方法中,会对项目的冷启动有一定的影响,而且,把所有的业务功能组件绑定势必造成不必要的内存常驻。
当然,有赞团队进行了很多的测试和思考,最终方案的敲定肯定是值得信赖的。
基于反射的接口调用封装
大家知道OC是支持反射的,比如:
Class className = NSClassFromString(@"Person");
SEL sel = NSSelectorFromString(@"getPersonName:");
...
然后可以通过- (id)performSelector:(SEL)aSelector
来完成消息的发送。
但是这种方式存在大量的硬编码,也无法触发编译器的自动补全,同时,只有在运行时才可以发现一些未知的错误。除此之外,无法实现多参数传值和方法返回值的获取的问题。所以这里选择NSInvocation
更合适
这里可以看下业界的 CTMediator 开源库。是基于Mediator
模式和Target-Action
模式来完成的
先说调用方法:(住:本次只讨论本地模块之间的调用,不考虑远程调用)
本地组件A在某处调用[[CTMediator sharedInstance] performTarget:targetName action:actionName params:@{...}]
向CTMediator
发起跨组件调用,CTMediator
根据获得的target和action信息
,通过objective-C
的runtime
转化生成target
实例以及对应的action
选择项,然后最终调用到目标业务提供的逻辑,完成需求。
组件仅需要通过Action
暴露可调用的接口即可。所有组件都通过组件自带的Target-Action
来响应,也就是说,模块与模块之间的接口被固化在了Target-Action
这一层,避免了实施组件化的改造过程中,对Business
的侵入,同时也提高了组件化接口的可维护性。
最后,调用者通过响应者给 CTMediator
做的 category
或者 extension
来发起调用,来避免使用字符串调用出现的不友好。
代码大家可以看下,可以说非常简洁了。考虑的也非常全面。关于一些架构的思想可以看下大佬casatwy
的文章iOS应用架构谈 组件化方案。
简化下代码如下:
// Mediator提供基于NSInvocation的接口调用方法的统一入口
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params;
// 业务模块对外提供的方法封装到Category中
@interface CTMediator (Goods)
- (NSArray *)goods_getGoodsList;
- (void)goods_gotoGoodsDetail:(NSString *)id;
...
@end
@impletation CTMediator(Goods)
- (NSArray *)goods_getGoodsList{
return [self performTarget:@“Target_Goods” action:@"getGoodsList" params:nil];
}
- (void *)goods_gotoGoodsDetail:(NSString *)id{
return [self performTarget:@“Target_Goods” action:@"gotoGoodsDetail" params:{@"id":id}];
}
@interface Target_Goods : NSObject
- (NSArray *)getGoodsList;
- (void)gotoGoodsDetail:(NSString *)id;
@end
基于面向协议思想的服务注册方案
每个模块提供自己的服务协议,然后将此协议声明注册到中间层。调用方能从中间层看到有哪些服务的接口,然后直接调用即可。
// 在中间件中完成协议的声明,方便所有模块调用和查阅
@protocol GoodsModuleService
- (NSArray*)getGoodsList;
- (NSInteger)getGoodsCount;
...
@end
// 在模块的load方法中,注册协议。并且让该模块实现协议中的方法
@interface GoodsModule : NSObject
@end
@implementation GoodsModule
+ (void)load {
[ServiceManager registerService:@protocol(GoodsModuleService) withModule:self.class]
}
//提供具体实现
- (NSArray*)getGoodsList {...}
- (NSInteger)getGoodsCount {...}
// 在其他模块中调用
id goodsModule = [ServiceManager objByService:@protocol(GoodsModuleService)];
NSArray *list = [goodsModule getGoodsList];
...
面向协议编程,看起来是很酷的。而且也不需要写反射代码。
但是:把协议的内容放到公共的地方,一旦发生改变,意味着用到协议的地方都要改一遍,而且,load
方法中进行协议绑定,还是会有老毛病存在。
有赞团队,对基于服务注册所消耗的启动时间做了测试,答案是影响可以忽略不计。
通知广播方案
基于通知的模块间通讯的方案,实现起来是最简单的,直接基于系统提供的NSNotificationCenter
即可。适合一对多的通讯场景。但是劣势也特别明显。复杂的数据传输,同步调用等方式都不太方便。通常用来作为以上几种方案的补充。
总结
组件化是项目发展到一定程度后的一种选择。如果不考虑时间成本和技术成本的话,是可以去搞一下的。虽然一开始的技术壁垒较高,但是对后续的迭代和升级是非常有帮助的。
多提一句。我们在做架构的时候,一定是基于自己的实际项目来搞的,业界很多优秀的思想可以拿来参考,没有必要生搬硬套。因为并没有绝对正确的架构,只有最适合自己的架构。
以上就是对组件化的一些整理和思考,希望对大家有帮助。