iOS开发-浅谈组件化方案

最近在学习组件化的一些方案。这里收集消化了一下,分享给大家参考。

组件化是架构层面的一个概念,它把项目按照某些规则(比如:按功能、按业务)划分成若干个颗粒度较小的单位,我们把这些单位称之为组件,或者是模块,来达到优化项目结构的目的。

组件又可以细分为 功能组件(如:图片库,网络库),业务组件也叫模块(如:订单模块,个人中心模块)

功能组件主要是物理层面的拆分,方便以后的复用

业务组件强调逻辑拆分,以便解耦

组件化的发展历程

开发之初,在功能方面,我们会把项目划分为基础层网络层数据层等等,而业务层面,仅仅是按照目录结构做一个简单的模块分层,比如订单模块个人中心模块

注:因为功能组件被大部分业务模块所依赖,这里暂时不讨论功能模块的架构问题。

随着业务的发展,项目变的越来越复杂,APP内各个业务之间的耦合严重,边界越来越模糊,经常会出现你中有我,我中有你的情况,如图

传统耦合图.jpg

可以看到,模块与模块严重耦合,对代码的扩展,以及代码的开发效率造成了巨大影响,有一种改一处动全身的感觉。发展到这个阶段,我们会把各个模块分割开来,通过中介者来完成不同模块之间的交互。如图

中介者.jpg

这个时候,架构看起来是清晰了许多。但因为中介者任然反向依赖业务模块,任然存在改一处多个模块受影响的情况,依赖仍旧是双向的。我们举个例子:

假如我现在在会员模块,要跳入到商品模块,此时,会员模块需要通过中介者来完成跳转

// 会员模块
[self.Intermediary gotoGoodsModuleWithParam:param]

// 在中介者中
- (void)gotoGoodsModuleWithParam:(id)param {
    self.goodsModule.param = param; // 参数
   [self.memberModule.navigationController pushViewController:self.goodsModule] 
}

假如此时我们的需求变了,除了传递参数外,还需要额外传一个参数来决定导航条的隐藏或者背景色,此时我们修改gotoGoodsModuleWithParam函数的结构,那么就需要修改两个地方,除了修改中介者中的方法以外,还需要修改模块中使用该函数的地方。

互相依赖其实是开发时候的大忌,我们还要考虑模块的循环引用问题、开发效率的问题、复用问题等等

如果消除中介者对模块的依赖呢?如图:

中间件.jpg

是不是有那味道了。各模块互不干涉,明确职责和边界。

那么如何实现这种功能呢?下面来看看业内的一些技术方案。

业内组件化方案

  1. 基于路由URL的UI页面统跳管理
  2. 基于反射的接口调用封装
  3. 基于面向协议思想的服务注册方案
  4. 基于通知的广播方案

基于路由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-Cruntime转化生成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即可。适合一对多的通讯场景。但是劣势也特别明显。复杂的数据传输,同步调用等方式都不太方便。通常用来作为以上几种方案的补充。

总结

组件化是项目发展到一定程度后的一种选择。如果不考虑时间成本和技术成本的话,是可以去搞一下的。虽然一开始的技术壁垒较高,但是对后续的迭代和升级是非常有帮助的。

多提一句。我们在做架构的时候,一定是基于自己的实际项目来搞的,业界很多优秀的思想可以拿来参考,没有必要生搬硬套。因为并没有绝对正确的架构,只有最适合自己的架构。

以上就是对组件化的一些整理和思考,希望对大家有帮助。

你可能感兴趣的:(iOS开发-浅谈组件化方案)