iOS组件化通用工具浅析
目录
- 1. 组件化是什么
- 2. 组件化的作用
- 3. 组件化实现
- 4. 中间件通用工具
- 5. BeeHive和CTMediator
1. 组件化是什么
这里的组件化一般是指业务模块化,简单来说就是将一个复杂的系统根据业务划分成不同的模块,这个没什么好说的,一般在做项目时,就已经做好了业务模块的划分。在讨论组件化时,其实只是在讨论如何在隔离各个业务模块情况下,实现模块间通信。(下文中的组件指的就是业务模块)
2. 组件化的作用
组件化的作用是可以实现组件隔离。
组件隔离,指的是各个组件之间不会有任何直接依赖,也就是说组件不会#import
另一个组件,各个组件在编译时是完全是解耦的。(组件间业务上的依赖是无法避免的)
这样,各个组件就可以单独开发和测试,而不需要依赖主工程,可以显著的提高团队的工作效率;
由于各个组件之间没有任何依赖,后期项目的维护也会相对容易一点。
3. 组件化实现
组件化的目的就是隔离组件,那么应该如何隔离,一般的解决方法是增加一个用于消息转发的中间层,通过这个中间层实现组件间通信,解耦各个组件。
下面使用Limboy文章中的例子来说明这个中间层的作用
增加中间层之前 增加中间层之后上述两图分别表示,在不使用中间层和使用中间层的情况下,组件间通信时,组件和中间层的依赖关系
不使用中间层的情况下,各个组件之间都是直接依赖,就是组件直接#import
被调用的组件,这些依赖关系凌乱而且复杂,在这种依赖关系下,如果想要多个组件并行开发,必须跟其他组件开发者做好接口约定,这里可能会有一份组件的接口文档,当组件接口出现变动时,需要通知所有此组件的使用者修改调用方法,这种情况下,后期维护会非常困难;
在使用中间层之后,所有的依赖关系都转接到中间层上了,所有的组件间通信都在中间层上集中处理,这样当组件出现变化时,只需要修改中间层就可以了。
下列是中间层Mediator
的代码实现:
//Mediator.m
#import "BookDetailComponent.h"
#import "ReviewComponent.h"
@implementation Mediator
+ (UIViewController *)BookDetailComponent_viewController:(NSString *)bookId {
return [BookDetailComponent detailViewController:bookId];
}
+ (UIViewController *)ReviewComponent_viewController:(NSString *)bookId reviewType:(NSInteger)type {
return [ReviewComponent reviewViewController:bookId type:type];
}
@end
到目前为止,已经初步实现组件化了,各个组件相互隔离,并且可以相互通信。
存在的问题
-
最显著的就是中间层的代码的维护问题,当项目中的组件越来越多,中间层的代码会越发膨胀,到那个时候,维护中间层可能会花费大量的时间。
-
另外就是中间层对组件存在依赖,这样的话,就很难将中间层抽取出来单独使用了,比如在新工程里面开发新组件的时候,想使用中间层,却发现需要引用其他所有的组件。
对于第一个问题,其解决方案一般是将中间层中的接口进行分类,让各个组件的创建者维护自己的中间层接口。
对于第二个问题,需要打破中间层对组件的依赖,然后再做一些异常判断。
对于上述两个问题,BeeHive和CTMediator这两个组件化工具都有一套完整的解决方案,下文中将通过分析这两个工具,来说明它们的具体步骤以及其内在联系。
4. 中间件通用工具
中间层的作用是帮助不同组件进行通信,它不可避免的会对组件形成依赖。虽然可以通过一些手段使得中间层与组件在编译层面上解耦了,但是中间层和组件仍然会存在业务上的关联。
换句话说,当使用中间层隔离各个组件时,中间层必然会与业务存在关联。
如果想要复用中间层,则必须将具体的业务逻辑剥离出中间层。在本文中,将剥离了具体业务的中间层称作中间件通用工具,BeeHive和CTMediator都是这种工具,下一节会讲到它们。
想要创建一个中间件通用工具,就需要搞清楚,中间层中哪些操作是业务相关的,哪些是非业务相关的。
组件间通信的流程可能有如下几个步骤:
-
调用者发起调用
组件调用者至少需要传递一个标识符给中间层,告诉中间层它想要调用哪个组件 -
中间层返回目标组件的句柄
根据调用者传入的标识符,中间层返回一个目标组件的句柄,使用这个组件句柄就可以和组件进行交互。
这个句柄可能是一个响应类,可能是一个可执行代码块,或者是其他可用来和目标组件交互的东西。 -
调用目标组件
通过使用这个句柄,可以和目标组件进行交互
从上述流程可知,中间层必定存在某种映射关系来指定标识符和组件句柄的对应关系,这种映射关系指定了组件的调用逻辑。
在这个流程中,与中间层相关的步骤如下:
-
生成映射关系
映射关系指定了组件的调用逻辑,生成这种映射关系的部分,必定与业务相关联。 -
存储映射关系
-
获取组件句柄
中间层一般是使用一个字典来存储这种映射关系,在存储和使用这种映射关系时,仅仅将它当做普通的对象来操作,所以通常[步骤2]和[步骤3]是与业务无关的。 -
使用组件句柄
如果组件句柄是要特定的上下文才能使用,比如是一个响应类,在使用句柄时,需要依赖于业务逻辑;
如果组件句柄不需要特定的上下文就能使用,比如是一个block,在使用句柄时,不需要依赖于业务逻辑。
上述四个步骤,[步骤1]与业务相关的,[步骤2]和[步骤3]两个步骤与业务无关,[步骤4]则需要看情况而定。
如果想要创建一个中间件通用工具,则必须将业务逻辑从中间层中剥离出来,然后中间层中剩余的逻辑就是中间件通用工具需要负责的部分了。
很明显,中间件通用工具可以包含[步骤2]和[步骤3],其功能如下:
- 将生成的映射关系存储起来
- 根据调用者传入的标识符,返回组件句柄
根据具体情况,中间件通用工具也可以包含[步骤4],负责直接使用组件句柄。
5. BeeHive和CTMediator
BeeHive和CTMediator是两个常用的中间件通用工具,它们的解决方案都比较成熟,下面简单解析一下这两个工具,看看他们是如何实现的。
5.1. BeeHive
BeeHive
使用protocol-impClass
方式来表示上文所说的映射关系,protocol
表示目标组件对外暴露的方法,impClass
表示目标组件的句柄。
BeeHive
内部使用一个可变字典来存储protocol-impClass
,其中protocol
作为key,impClass
作为value;
在调用组件时,调用者将目标组件的协议protocol
作为参数传给BeeHive
,然后BeeHive
返回对应的组件句柄impClass
。
5.1.1. 构建中间层
(构建中间层等同于上节中的前两个步骤:生成映射关系和存储映射关系)
在BeeHive
中,中间层由协议protocol
、协议对应的响应类impClass
以及BeeHive
组成。
在使用BeeHive
调用组件之前,需要使用BeeHive
构建中间层,一般分为以下两步:(下列代码来自BeeHive
项目中的demo)
- 声明组件协议
定义一个协议protocol
,在这个协议中声明组件对外暴露的方法,每一个组件对应一个协议protocol
//创建协议
//TradeServiceProtocol.h
#import "BHServiceProtocol.h"
@protocol TradeServiceProtocol <NSObject, BHServiceProtocol>
@property(nonatomic, strong) NSString *itemId;
@end
- 注册映射关系
在组件中指定一个类作为其实现类impClass
,这个实现类需要遵守这个协议protocol
,然后使用BeeHive
提供的方法将protocol-impClass
这种映射关系注册到BeeHive
中。
可以在BeeHive
之外的任何地方注册,只需要在调用组件之前注册就行了。
//注册protocol-impClass映射关系
#import "BHService.h"
[[BeeHive shareInstance] registerService:@protocol(TradeServiceProtocol) service:[BHTradeViewController class]];
BeeHive
本身并不会生成映射关系,它只是提供注册方法给调用者使用,真正生成映射关系的是BeeHive
的调用者,BeeHive
本身没有依赖具体的组件。
BeeHive
内部使用一个可变字典来存储protocol-impClass
映射关系,它并不关心protocol
和impClass
是否和组件有关,它唯一的要求是protocol
和impClass
必须有值,且impClass
必须遵循协议protocol
。
也就是说,在生成映射关系和存储映射关系这两个步骤中,BeeHive
只负责后者,然后BeeHive
提供生成前者的接口,具体生成前者的操作不是由BeeHive
执行。
5.1.2. 调用组件
(调用组件等同于上一节中的后两个步骤:获取组件句柄和使用组件句柄)
在调用组件时,调用者将目标组件的协议protocol
作为参数传给BeeHive
,根据上述注册的映射关系protocol-impClass
,获取协议protocol
对应的实现类impClass
,也就是说调用者需要依赖这个协议protocol
,然后调用者就可以使用这个实现类来访问目标组件了。
//BHViewController.m
#import "BHService.h"
...
id<TradeServiceProtocol> v2 = [[BeeHive shareInstance]createService:@protocol(TradeServiceProtocol)];
if ([v2 isKindOfClass:[UIViewController class]]) {
v2.itemId = @"sdfsdfsfasf";
}
...
当调用者使用BeeHive
调用组件时,BeeHive
根据协议protocol
获取对应的实现类impClass
,BeeHive
只是将这个实现类impClass
当做一个普通的Class
类型,然后返回这个实现类给调用者。这里的实现类impClass
就是组件句柄,所以在获取组件句柄的时候,BeeHive
和业务是没有依赖的。
至于如何使用实现类impClass
,那是调用者负责的,BeeHive
并不关心。
也就是说,在获取组件句柄和使用组件句柄这两个步骤中,BeeHive
只负责前者,而后者是由组件的调用者执行的。
根据以上分析,BeeHive
完全负责中间件通用工具的标准。
5.2. CTMediator
CTMediator
内部是使用下列runtime
方法实现的
- (id)performSelector:(SEL)aSelector withObject:(id)object;
在调用目标组件时,调用者将组件响应类的类名和方法名作为参数传给CTMediator
,CTMediator
通过上述方法调用目标组件。
在CTMediator
中,由于只需要响应类的类名和方法名就能调用组件,所以这里将响应类的类名和方法名当做组件句柄。
在CTMediator
中,组件的响应类被称作target-action
。
5.2.1. 创建中间层
在CTMediator
中,中间层是由target-action
、CTMediator
和其分类共同组成的。
target-action
代表组件对外的接口,CTMediator
的分类是面向调用者的接口,CTMediator
则负责将这二者关联起来。
- 创建
target-action
针对每个组件创建一个target类,其内部定义了组件对外暴露的action(方法)。和组件通信时,其实质是调用一个特定的target-action
的方法。
target
类的类名必须以Target_
开头,比如Target_A
,action
的方法名必须以Action_
开头,比如Action_nativeFetchDetailViewController
。
创建一个target-action
(下列代码来自CTMediator
项目中的demo)
//Target_A.h
@interface Target_A : NSObject
- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params;
@end
//Target_A.m
#import "Target_A.h"
#import "DemoModuleADetailViewController.h"
@implementation Target_A
- (UIViewController *)Action_nativeFetchDetailViewController:(NSDictionary *)params
{
// 因为action是从属于ModuleA的,所以action直接可以使用ModuleA里的所有声明
DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
viewController.valueLabel.text = params[@"key"];
return viewController;
}
@end
- 创建
CTMediator
的分类
CTMediator
分类是面向组件调用者的,每一个组件都有一个对应的CTMediator
分类,调用者使用这个分类的接口来和组件通信。
CTMediator
分类中每一个方法内部都会调用一个或多个target-action
的方法,调用者使用分类方法来调用组件时,其最终目的是调用特定的target-action
的方法。
创建一个CTMediator
的分类(下列代码来自CTMediator
项目中的demo)
//CTMediator+CTMediatorModuleAActions.h
#import "CTMediator.h"
@interface CTMediator (CTMediatorModuleAActions)
- (UIViewController *)CTMediator_viewControllerForDetail;
@end
//CTMediator+CTMediatorModuleAActions.m
#import "CTMediator+CTMediatorModuleAActions.h"
NSString * const kCTMediatorTargetA = @"A";
NSString * const kCTMediatorActionNativFetchDetailViewController = @"nativeFetchDetailViewController";
@implementation CTMediator (CTMediatorModuleAActions)
- (UIViewController *)CTMediator_viewControllerForDetail
{
UIViewController *viewController = [self performTarget:kCTMediatorTargetA
action:kCTMediatorActionNativFetchDetailViewController
params:@{@"key":@"value"}
shouldCacheTarget:NO
];
if ([viewController isKindOfClass:[UIViewController class]]) {
// view controller 交付出去之后,可以由外界选择是push还是present
return viewController;
} else {
// 这里处理异常场景,具体如何处理取决于产品
return [[UIViewController alloc] init];
}
}
@end
CTMediator的分类
中的每一个方法都会调用特定的target-action
的方法,这种调用关系被写死在代码中,属于硬编码,它表示中间层标识符-组件句柄
的映射关系。
定义CTMediator的分类
的方法的过程可以看做是生成这种映射关系的过程。
CTMediator的分类
是由组件作者创建的,CTMediator
不会对它产生依赖。
也就是说,生成映射关系和存储映射关系这两个步骤都是由CTMediator分类
负责的。
5.2.2. 调用组件
调用组件时,需要引用之前定义的分类,然后去这个分类的头文件中找到想要执行的方法,最后执行这个方法。
调用者只需要依赖CTMediator
的分类,就可以完成组件间通信了。
//ViewController.m
#import "CTMediator+CTMediatorModuleAActions.h"
...
UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];
[self presentViewController:viewController animated:YES completion:nil];
...
在调用组件时,调用者只需调用对应CTMediator的分类
的方法,然后CTMediator的分类
根据映射关系获取组件句柄,也就是target和action的字符串名称,再将组件句柄传给CTMediator
;
CTMediator
接受到组件句柄后,执行对应的target-action
的方法。
从这个角度来说,CTMediator
实现了获取组件句柄和使用组件句柄这两个步骤。
5.3. 其他
5.3.1. CTMediator分类的作用
下列代码没有使用分类,其效果和上面使用分类的代码等同
UIViewController *viewController = [[CTMediator sharedInstance]performTarget:@"A" action:@"nativeFetchDetailViewController" params:@{@"key":@"value"} shouldCacheTarget:NO];
if (![viewController isKindOfClass:[UIViewController class]]) {
viewController = [[UIViewController alloc] init];
}
[self presentViewController:viewController animated:YES completion:nil];
可以看出,上述调用代码比较繁琐,调用者需要记住target和action的字符串名称,然后手动输入,这对于调用者来说是不太友好的;传入的参数是一个字典,调用者无法直观的知道方法所需的具体参数,而且调用组件的逻辑会分散在项目各处,可读性很差。使用CTMediator的分类
可以统一调用入口,并提供可读性强的接口。
5.3.2. BeeHive的protocol
和CTMediator的category
的异同
-
相同
BeeHive
中的protocol
和CTMediator
中的category
有一些相似之处,它们都包含了中间层对外的接口,而且它们和组件的关系也是一对一的,从这一点上来看,它们在功能上是一致的。 -
不同
如果没有protocol
,则中间层无法生成标识符-组件句柄
的映射关系,调用者在不(编译层)依赖组件句柄的情况下,不可以拿到组件句柄。对于BeeHive
来说,protocol
是不可或缺的;
如果没有category
,调用者在不(编译层)依赖组件句柄的情况下,可以拿到组件句柄,因为组件句柄只是两个字符串。对于CTMediator
来说,category
不是必须的。