浅谈对组件化方案的理解 - AlvinSunny/OC-TheUnderlying GitHub Wiki
业务组件化(或者叫模块化)作为移动端应用架构的主流方式之一,它把项目按照某些规则(比如:按功能、按业务)拆分成若干个颗粒度比较小的单位,我们把这些单位称之为组件/模块,来达到优化项目结构的目的
组件又可以按分层概念大致分为基础组件、弱业务/功能组件、业务组件,弱业务组件的拆分是为了在业务层能够很好的复用,业务组件强调逻辑拆分以便解耦
组件化的发展历程
传统的 App 架构设计更多强调的是分层,基于设计模式六大原则之一的单一职责原则,将系统划分为基础层,网络层,UI层等等,以便于维护和扩展。但随着业务的发展,系统变得越来越复杂,只做分层就不够了。App 内各子系统之间耦合严重, 边界越来越模糊,经常发生你中有我我中有你的情况(图一)。这对代码质量,功能扩展,以及开发效率都会造成很大的影响。此时,一般会将各个子系统划分为相对独立的模块,通过中介者模式收敛交互代码,把模块间交互部分进行集中封装, 所有模块间调用均通过中介者来做(图二)。这时架构逻辑会清晰很多,但因为中介者仍然需要反向依赖业务模块,这并没有从根本上解除循坏依赖等问题。时不时发生一个模块进行改动,多个模块受影响编译不过的情况。进一步的,通过技术手段,消除中介者对业务模块依赖,即形成了业务模块化架构设计(图三)。
通过业务模块化架构,一般可以达到明确模块职责及边界,提升代码质量,减少复杂依赖,优化编译速度,提升开发效率等效果
业内组件化方案
1、基于路由URL的UI页面统跳管理
2、基于反射的接口调用封装
3、基于面向协议思想的服务注册方案
4、基于通知的广播方案
基于路由URL的UI页面统跳管理
关于URL业内两个比较经典的案例有赞-Bifrost、蘑菇街-MGJRouter
不带参数情况示例
有赞-Bifrost
// kRouteGoodsDetail = @"/goods/goods_detail"
UIViewController * vc = [Router handleURL:kRouteGoodsDetail];
if (vc) [self.navigationController pushViewController:vc animated:YES];
蘑菇街-MGJRouter
[MGJRouter registerURLPattern:@"mgj://foo/bar" toHandler:^(NSDictionary *routerParameters) {
NSLog(@"routerParameterUserInfo:%@", routerParameters[MGJRouterParameterUserInfo]);
}];
[MGJRouter openURL:@"mgj://foo/bar"];
带参数情况示例
有赞-Bifrost
//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];
}
蘑菇街-MGJRouter
[MGJRouter registerURLPattern:@"mgj://search/:query" toHandler:^(NSDictionary *routerParameters) {
NSLog(@"routerParameters[query]:%@", routerParameters[@"query"]); // bicycle
NSLog(@"routerParameters[color]:%@", routerParameters[@"color"]); // red
}];
[MGJRouter openURL:@"mgj://search/bicycle?color=red"];
复杂的参数情况示例
有赞-Bifrost
+ (nullable id)handleURL:(nonnull NSString *)urlStr
complexParams:(nullable NSDictionary*)complexParams
completion:(nullable RouteCompletion)completion;
蘑菇街-MGJRouter
[MGJRouter registerURLPattern:@"mgj://category/travel" toHandler:^(NSDictionary *routerParameters) {
NSLog(@"routerParameters[MGJRouterParameterUserInfo]:%@", routerParameters[MGJRouterParameterUserInfo]);
// @{@"user_id": @1900}
}];
[MGJRouter openURL:@"mgj://category/travel" withUserInfo:@{@"user_id": @1900} completion:nil];
URL 路由的优点
- 极高的动态性,适合经常开展运营活动的app
- 方便地统一管理多平台的路由规则
- 易于适配URL Scheme,可以下发
URl 路由的缺点
- 传参方式有限,并且无法利用编译器进行参数类型检查,因此所有的参数都是通过字符串转换而来
- 只适用于界面模块,不适用于通用模块
- 参数的格式不明确,是个灵活的 dictionary,也需要有个地方可以查参数格式。
- 不支持storyboard
- 依赖于字符串硬编码,难以管理,蘑菇街做了个后台专门管理。
- 无法保证所使用的的模块一定存在
- 解耦能力有限,url 的”注册”、”实现”、”使用”必须用相同的字符规则,一旦任何一方做出修改都会导致其他方的代码失效,并且重构难度大
基于反射的接口调用封装
大家知道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接口,无需再重复封装
缺点
- 用框架来创建所有对象,创建方式不同,即不支持外部传入参数
- 用OC runtime创建对象,不支持swift
- 只做了protocol 和 class 的匹配,不支持更复杂的创建方式 和依赖注入
- 无法保证所使用的protocol 一定存在对应的模块,也无法直接判断某个protocol是否能用于获取模块
基于面向协议思想的服务注册方案
每个模块提供自己的服务协议,然后将此协议声明注册到中间层。调用方能从中间层看到有哪些服务的接口,然后直接调用即可
protocol比较典型的三方框架就是阿里的BeeHive。BeeHive借鉴了Spring Service、Apache DSO的架构理念,采用AOP+扩展App生命周期API形式,将业务功能、基础功能模块以模块方式以解决大型应用中的复杂问题,并让模块之间以Service形式调用,将复杂问题切分,以AOP方式模块化服务。
BeeHive 核心思想
1、各个模块间调用从直接调用对应模块,变成调用Service的形式,避免了直接依赖。 2、App生命周期的分发,将耦合在AppDelegate中逻辑拆分,每个模块以微应用的形式独立存在。
// 在中间件中完成协议的声明,方便所有模块调用和查阅
@protocol GoodsModuleService
- (NSArray*)getGoodsList;
- (NSInteger)getGoodsCount;
...
@end
// 在模块的load方法中,注册协议。并且让该模块实现协议中的方法
@interface GoodsModule : NSObject<GoodsModuleService>
@end
@implementation GoodsModule
+ (void)load {
[ServiceManager registerService:@protocol(GoodsModuleService) withModule:self.class]
}
//提供具体实现
- (NSArray*)getGoodsList {...}
- (NSInteger)getGoodsCount {...}
// 在其他模块中调用
id<GoodsModuleService> goodsModule = [ServiceManager objByService:@protocol(GoodsModuleService)];
NSArray *list = [goodsModule getGoodsList];
...
面向协议编程,看起来是很酷的。而且也不需要写反射代码。
但是:把协议的内容放到公共的地方,一旦发生改变,意味着用到协议的地方都要改一遍,而且,load方法中进行协议绑定,还是会有老毛病存在。
有赞团队,对基于服务注册所消耗的启动时间做了测试,答案是影响可以忽略不计。
优点
1、利用接口调用,实现了参数传递时的类型安全 2、直接使用模块的protocol接口,无需再重复封装
缺点
1、用框架来创建所有对象,创建方式不同,即不支持外部传入参数 2、用OC runtime创建对象,不支持swift 3、只做了protocol 和 class 的匹配,不支持更复杂的创建方式 和依赖注入 4、无法保证所使用的protocol 一定存在对应的模块,也无法直接判断某个protocol是否能用于获取模块
基于通知的广播方案
基于通知的模块间通讯的方案,实现起来是最简单的,直接基于系统提供的NSNotificationCenter即可。适合一对多的通讯场景。但是劣势也特别明显。复杂的数据传输,同步调用等方式都不太方便。通常用来作为以上几种方案的补充。
总结
移动应用的业务模块化架构设计,其真正的目标是提升开发质量和效率。单从实现角度来看并没有什么黑魔法或技术难点,更多的是结合团队实际开发协作方式和业务场景的具体考量——“适合自己的才是最好的”。有赞移动团队通过过往3年的实践,发现一味的追求性能,绝对的追求模块间编译隔离,过早的追求模块代码管理隔离等方式都偏离了模块化设计的真正目的,是得不偿失的。更合适的方式是在可控的改造代价下,一定程度考虑未来的优化方式,更多的考虑当前的实际场景,来设计适合自己的模块化方式。希望大家都能找到适合自己应用的业务模块化之路
附优秀的组件化文章