iOS组件化通用工具浅析

作者: wangzzzzz | 来源:发表于2018-05-10 16:07 被阅读410次

    目录

    • 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不是必须的。

    相关文章

      网友评论

        本文标题:iOS组件化通用工具浅析

        本文链接:https://www.haomeiwen.com/subject/ehosrftx.html