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

到目前为止,已经初步实现组件化了,各个组件相互隔离,并且可以相互通信。

存在的问题
  1. 最显著的就是中间层的代码的维护问题,当项目中的组件越来越多,中间层的代码会越发膨胀,到那个时候,维护中间层可能会花费大量的时间。

  2. 另外就是中间层对组件存在依赖,这样的话,就很难将中间层抽取出来单独使用了,比如在新工程里面开发新组件的时候,想使用中间层,却发现需要引用其他所有的组件。

对于第一个问题,其解决方案一般是将中间层中的接口进行分类,让各个组件的创建者维护自己的中间层接口。
对于第二个问题,需要打破中间层对组件的依赖,然后再做一些异常判断。

对于上述两个问题,BeeHiveCTMediator这两个组件化工具都有一套完整的解决方案,下文中将通过分析这两个工具,来说明它们的具体步骤以及其内在联系。

4. 中间件通用工具

中间层的作用是帮助不同组件进行通信,它不可避免的会对组件形成依赖。虽然可以通过一些手段使得中间层与组件在编译层面上解耦了,但是中间层和组件仍然会存在业务上的关联。
换句话说,当使用中间层隔离各个组件时,中间层必然会与业务存在关联。

如果想要复用中间层,则必须将具体的业务逻辑剥离出中间层。在本文中,将剥离了具体业务的中间层称作中间件通用工具,BeeHiveCTMediator都是这种工具,下一节会讲到它们。

想要创建一个中间件通用工具,就需要搞清楚,中间层中哪些操作是业务相关的,哪些是非业务相关的。

组件间通信的流程可能有如下几个步骤:


  1. 调用者发起调用
    组件调用者至少需要传递一个标识符给中间层,告诉中间层它想要调用哪个组件

  2. 中间层返回目标组件的句柄
    根据调用者传入的标识符,中间层返回一个目标组件的句柄,使用这个组件句柄就可以和组件进行交互。
    这个句柄可能是一个响应类,可能是一个可执行代码块,或者是其他可用来和目标组件交互的东西。

  3. 调用目标组件
    通过使用这个句柄,可以和目标组件进行交互

从上述流程可知,中间层必定存在某种映射关系来指定标识符和组件句柄的对应关系,这种映射关系指定了组件的调用逻辑。

在这个流程中,与中间层相关的步骤如下:


  1. 生成映射关系
    映射关系指定了组件的调用逻辑,生成这种映射关系的部分,必定与业务相关联。

  2. 存储映射关系

  3. 获取组件句柄
    中间层一般是使用一个字典来存储这种映射关系,在存储和使用这种映射关系时,仅仅将它当做普通的对象来操作,所以通常[步骤2]和[步骤3]是与业务无关的。

  4. 使用组件句柄
    如果组件句柄是要特定的上下文才能使用,比如是一个响应类,在使用句柄时,需要依赖于业务逻辑;
    如果组件句柄不需要特定的上下文就能使用,比如是一个block,在使用句柄时,不需要依赖于业务逻辑。

上述四个步骤,[步骤1]与业务相关的,[步骤2]和[步骤3]两个步骤与业务无关,[步骤4]则需要看情况而定。

如果想要创建一个中间件通用工具,则必须将业务逻辑从中间层中剥离出来,然后中间层中剩余的逻辑就是中间件通用工具需要负责的部分了。
很明显,中间件通用工具可以包含[步骤2]和[步骤3],其功能如下:

  1. 将生成的映射关系存储起来
  2. 根据调用者传入的标识符,返回组件句柄

根据具体情况,中间件通用工具也可以包含[步骤4],负责直接使用组件句柄。

5. BeeHive和CTMediator

BeeHiveCTMediator是两个常用的中间件通用工具,它们的解决方案都比较成熟,下面简单解析一下这两个工具,看看他们是如何实现的。

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

  1. 声明组件协议
    定义一个协议protocol,在这个协议中声明组件对外暴露的方法,每一个组件对应一个协议protocol
//创建协议
//TradeServiceProtocol.h
#import "BHServiceProtocol.h"

@protocol TradeServiceProtocol <NSObject, BHServiceProtocol>


@property(nonatomic, strong) NSString *itemId;


@end
  1. 注册映射关系
    在组件中指定一个类作为其实现类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映射关系,它并不关心protocolimpClass是否和组件有关,它唯一的要求是protocolimpClass必须有值,且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获取对应的实现类impClassBeeHive只是将这个实现类impClass当做一个普通的Class类型,然后返回这个实现类给调用者。这里的实现类impClass就是组件句柄,所以在获取组件句柄的时候,BeeHive和业务是没有依赖的。
至于如何使用实现类impClass,那是调用者负责的,BeeHive并不关心。

也就是说,在获取组件句柄使用组件句柄这两个步骤中,BeeHive只负责前者,而后者是由组件的调用者执行的。

根据以上分析,BeeHive完全负责中间件通用工具的标准。

5.2. CTMediator

CTMediator内部是使用下列runtime方法实现的

- (id)performSelector:(SEL)aSelector withObject:(id)object;

在调用目标组件时,调用者将组件响应类的类名和方法名作为参数传给CTMediatorCTMediator通过上述方法调用目标组件。
CTMediator中,由于只需要响应类的类名和方法名就能调用组件,所以这里将响应类的类名和方法名当做组件句柄。
CTMediator中,组件的响应类被称作target-action

5.2.1. 创建中间层

CTMediator中,中间层是由target-actionCTMediator和其分类共同组成的。
target-action代表组件对外的接口,CTMediator的分类是面向调用者的接口,CTMediator则负责将这二者关联起来。

  1. 创建target-action
    针对每个组件创建一个target类,其内部定义了组件对外暴露的action(方法)。和组件通信时,其实质是调用一个特定的target-action的方法。
    target类的类名必须以Target_开头,比如Target_Aaction的方法名必须以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
  1. 创建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中的protocolCTMediator中的category有一些相似之处,它们都包含了中间层对外的接口,而且它们和组件的关系也是一对一的,从这一点上来看,它们在功能上是一致的。

  • 不同
    如果没有protocol,则中间层无法生成标识符-组件句柄的映射关系,调用者在不(编译层)依赖组件句柄的情况下,不可以拿到组件句柄。对于BeeHive来说,protocol是不可或缺的;
    如果没有category,调用者在不(编译层)依赖组件句柄的情况下,可以拿到组件句柄,因为组件句柄只是两个字符串。对于CTMediator来说,category不是必须的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容