使用CTMediators实现组件化探索

3,194 阅读4分钟

本文主要是记录使用CTMediators框架进行组件化探索以及实践的过程

1.组件化的基本分层

  • 通常组件化的分层思想大致分为以下三层:
    • 基础模块
    • 通用模块
    • 业务模块
  • 对于模块的开发集成顺序大致为从下至上,即从基础模块通用模块业务模块,但是依赖的顺序是从上到下,业务 依赖 通用通用 依赖 基础
集成顺序
`基础模块` -->   `通用模块` -->  `业务模块`

依赖顺序
`基础模块` <--  `通用模块` <--  `业务模块`
  • 基础模块通用模块的分层方式这个见仁见智,可以看做是同一个层次,也可以是两个层次
    • 基础模块主要是封装一些不与业务相关的模块,比如一些分类,工具类,这里明确一点就是不和业务挂钩,简单来说这一层放在其他项目时,不需要做修改就可以使用,这样是最理想的
    • 通用模块这里实际应该叫通用业务模块,这一层主要体现通用,其次体现面向业务。比如一些公用组件,比如通用UIButton,瀑布流,与业务挂钩的分类,时间的计算NSDate相关的。这一层一定是体现与业务挂钩的通用,如果完全不依赖业务逻辑的,就放在基础模块

2.业务模块

其实组件化除去技术层面的东西之外,业务模块的设计是最难的,必须着眼于整个项目,将项目如何划分,既要考虑到对目前分层的合理性,又要考虑以后的拓展性。因为如果一个模块如果频繁的进行改动,就要合理的设计前端的接口,与其他模块的交互等等。因为本文主要讲解CTM框架,这一层完全依赖于不同APP的业务,所以这里不做具体的展开了

  1. 通常项目模块间的关系大致如下图,就是我们在进行组件化之前的项目,给个层次中间的关系,这里写的模块可以理解是类之间的关系
  • 各个模块都或多或少有关系,模块间进行通讯(即类之间的方法调用),需要进行#import导入头文件,是一种比较强的耦合关系
  • 模块间进行组件化首先要解决的问题就是模块间之间的耦合关系,只要耦合关系越低,模块才能越独立
  • 结局的基本的思路是建立通讯中间层,各个模块之间通过中间层进行中专,模块之间不直接进行联系。如下图
  • 但是这种中间层的设计,就会引入一个新的问题,
    • 各个模块于中间层的强耦合,他需要导入所以模块,才能使模块进行通讯。
    • 造成这样中间层就是比较庞大。比如A模块只想和C模块通讯,这时中间层只需要设计为包含AC即可,但是目前中间拥有ABCD所有的代码,这里不合理
    • 一个思路 就是讲中间层在进行分层,这样可能就会有一个排列组合的关系,AB AC AD BC BD CD ,能不能对中间层进行优化,这里我们先埋个伏笔

3.CTMediator 通讯中间框架介绍

  • 框架地址: github.com/casatwy/CTM…

  • 代码结构图:

  • 框架使用前提:已经对项目划分好合理的业务模块,单纯是对项目的业务进行分层,不考模块之间的通讯

  • 框架的基本思路是

    • 将每个模块隔离出一个独立Target层(这个和OC的target-action模式没有任何关系),该层就是一个单独的类,可以理解为对于模块的声明,类比OC文件中的.h 和 .m之间的关系,Target层为h 模块为m
    • Target层 作为该模块通讯的入口,通俗的讲就是这个模块的声明文件,对该模块的方法调用入口进行一次封装,就是说模块可以提供给外部的功能。比如登录模块,可以提供登录功能,注册,忘记密码等等功能,那我们就把这些方法暴露到Target层里,外部调用Target层方法,然后间接调用到模块内部
    • Target层的另外一个作用就是,在代码封装过程可以进行一些业务层面的判断或者容错,比如外部没有按照合理的规则传入参数,我们可以先一步进行拦截判断,这里也可以将模块中的容错逻辑放到Target层
  • 框架代码思路:完全杜绝耦合--runtime

    • 将方法的调用 转换为 不导入某模块的情况下去调用该模块的方法,避免模块间导入
    • 以类名( 字符串),方法名( 字符串)进行调用,通过方法签名 + NSInvocation 方式对方法调用进行封装,通过performSelector 进行调用,runtime将字符串解析为对应的类 或者方法,这样比如我们想调用A类的方法B,就不需要导入A类,直接用@“A” 和 @“B”,通过runtime进行转换为具体的类和方法(后续我们会进行源码分析)
    • 由于需要对字符串或者方法,这样会无可避免的出现手误,所以对于参数,比如target(), action(),等进行容错处理,框架内部进行了处理

4.增加分类 -- 解决中间层于模块间的耦合,减少中间层排列组合问题

4.1总体介绍

设计分类大致有两个作用

  • 防止CTM框架的发生意外 尽管CTMediator的代码整体比较少,逻辑也不是很复杂,但是为了避免框架的代码发生过大的变化,不直接修改CTM的源码进行调用,通过对其创建分类进行拓展,这种思想在我们使用其他三方框架的时候也有体现,比如在使用AFN等网络框架,我们不会直接对框架内代码直接调用,而是独立一个类来管理和封装AFN,通过这个类于外部进行交互

  • 避免项目或者模块对CTMediator框架进行强耦合

    • 我们为每一个模块独立出一个类,分类中的方法与Target层声明的方法进行一一对应。
    • 还拿登录模块进行举例,我们把登录,注册,忘记密码等功能在Target层中已经写好了声明和实现,然后我们将这些声明在合理的复制到分类。
    • 对于外部,这个分类就是这个模块的声明文件,如果你想使用登录,注册等方法的时候,只需要导入这个分类就可以,这和我们通常意义中使用分类效果是一样。
    • 所以一个完整的模块是包含 分类 --> Target层 ---> 模块源码层【见上图】

4.2 CTMediator代码 和 demo介绍

下面我们针对在github给出的demo进行介绍

1. 项目整体介绍
  • 项目代码结构
  • 代码运行起来,是一个tableView
  • 我们以 present image 这个操作
2. 这个功能是将 DemoModuleADetailViewController modal出来,并需要往这个控制器里传递一个UIImage,DemoModuleADetailViewController 以下简称为模块A
  • 模块A 声明暴露了一个imageView,用于传值
@interface DemoModuleADetailViewController : UIViewController
@property (nonatomic, strong, readonly) UILabel *valueLabel;
@property (nonatomic, strong, readonly) UIImageView *imageView;
@end
  • 模块A 的target 层 Target_A类对应的方法及其实现为如下,主要是创建模块A控制器,解析传进来的参数解,并进行赋值控制器,并实现modal
- (id)Action_nativePresentImage:(NSDictionary *)params
{
    DemoModuleADetailViewController *viewController = [[DemoModuleADetailViewController alloc] init];
    viewController.valueLabel.text = @"this is image";
    viewController.imageView.image = params[@"image"];
    [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:viewController animated:YES completion:nil];
    return nil;
}
  • 方法名 Action_nativePresentImage根据是根据目前CTM的规则拼接出来,方法名的规则是Action_ 拼上方法名,nativePresentImage是我们可以定义的方法的名字,这名字在分类的层面进行声明拼接,在demo是定义成了一个static string.我的理解起名和如何定义字符串,只要项目内部约定好就行,大家都根据这规则就好。
3. 第二步解决了方法名的问题,然后我们整体看下方法的调用,从cell的点击一步一步
  • cell 点击,调用CTM分类方法
  • 分类方法的实现
  • 调用到CTM内部方法,进行类 和 方法名的转换,创建对象,内部应做了相应的注释,下一步对 performSelector: 进一步封装
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
    // tagrt判空
    if (targetName == nil || actionName == nil) {
        return nil;
    }
    // 对swift的特殊标记
    NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
    
    // 拼接target-action的tagert,就是方法调用的类名的字符串,类名规则为Target_方法名
    NSString *targetClassString = nil;
    if (swiftModuleName.length > 0) {
        targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
    } else {
        // 类名规则为Target_方法名
        targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    }
    
    // 做了类对象的缓存,避免多次创建对象
    NSObject *target = self.cachedTarget[targetClassString];
    // 类对象
    if (target == nil) {
        Class targetClass = NSClassFromString(targetClassString);
        target = [[targetClass alloc] init];
    }
    // 处理方法名字 拼接规则为 Action_方法名
    NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
    // 方法名转 SEL
    SEL action = NSSelectorFromString(actionString);
    // 容错处理
    if (target == nil) {
        // 这里是处理无响应请求的地方之一,这个demo做得比较简单,如果没有可以响应的target,就直接return了。实际开发过程中是可以事先给一个固定的target专门用于在这个时候顶上,然后处理这种请求的
        [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
        return nil;
    }
    // 缓存对象
    if (shouldCacheTarget) {
        self.cachedTarget[targetClassString] = target;
    }

    if ([target respondsToSelector:action]) {
        // 底层方法调用
        return [self safePerformAction:action target:target params:params];
    } else {
        // 这里是处理无响应请求的地方,如果无响应,则尝试调用对应target的notFound方法统一处理
        SEL action = NSSelectorFromString(@"notFound:");
        if ([target respondsToSelector:action]) {
            return [self safePerformAction:action target:target params:params];
        } else {
            // 这里也是处理无响应请求的地方,在notFound都没有的时候,这个demo是直接return了。实际开发过程中,可以用前面提到的固定的target顶上的。
            [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
            [self.cachedTarget removeObjectForKey:targetClassString];
            return nil;
        }
    }
}
  • 此部分主要是对参数进行容错,对performSelector进一步封装,并返回方法调用方的对象
- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
{
    NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
    if(methodSig == nil) {
        return nil;
    }
    const char* retType = [methodSig methodReturnType];

    if (strcmp(retType, @encode(void)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        return nil;
    }

    if (strcmp(retType, @encode(NSInteger)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        NSInteger result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

    if (strcmp(retType, @encode(BOOL)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        BOOL result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

    if (strcmp(retType, @encode(CGFloat)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        CGFloat result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

    if (strcmp(retType, @encode(NSUInteger)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        NSUInteger result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
}

至此一个完整CTM调用就结束了,大致的流程为