iOS组件化的分层思路实践

iOS组件化的分层思路

组件化是一种软件设计方法,它将一个大型应用程序拆分成多个独立且可重用的模块。这些模块可以分别开发、测试和维护,从而提高代码的复用性和可维护性。通常,组件化的分层思想大致分为以下三层:

  1. 基础模块:封装一些不与业务相关的模块,如工具类和分类。这一层的代码应当是完全独立于项目,可以在其他项目中直接使用而无需修改。
  2. 通用模块:实际上是"通用业务模块",既包含通用代码,也包含与业务逻辑相关的通用部分,如通用UIButton组件等。
  3. 业务模块:具体业务逻辑的实现模块,需要结合实际项目进行划分。

模块的开发集成顺序为从下至上,即从基础模块到通用模块再到业务模块,而依赖顺序则是从上到下。

业务模块的设计

业务模块的设计是组件化过程中最难的一部分,需要综合考虑当前的合理性和未来的扩展性。在组件化之前,项目中的各个模块可能会有复杂的耦合关系。在进行组件化之后,需要通过建立"通讯中间层"来降低这些模块之间的耦合度。

CTMediator 通讯中间框架介绍

CTMediator是一个用于模块间通讯的中间层框架,通过runtime机制实现完全杜绝模块间的耦合。它的基本思路包括: • 每个模块隔离出一个独立的Target层,这个层是模块的声明文件。该层提供了外部调用该模块功能的接口,并对传入参数进行验证和错误处理。 • 使用字符串形式的类名和方法名,通过runtime机制在不导入某模块的情况下调用其方法,从而避免模块间的硬性依赖。

增加分类 - 解决中间层与模块间的耦合

为防止CTMediator框架发生意外变化及避免项目或模块对框架的强耦合,可以通过增加分类(Category)进行拓展。其好处包括:

  1. 防止框架变化:通过创建分类进行拓展,不直接修改CTMediator源码,避免因框架升级导致的问题。
  2. 减少强耦合:为每个模块独立出一个类,分类中的方法与Target层声明的方法对应,方便外部调用。这使一个完整模块包括 分类 --> Target层 ---> 模块源码层。

框架使用示例

以present image操作为例,模块A暴露了一个imageView用于传值。模块A的Target层实现了所需方法,如Action_nativePresentImage。最终,通过CTMediator框架的performSelector方法实现对目标方法的调用。 总结 • CTMediator通过runtime机制进行模块解耦。 • 增加分类进一步减少中间层与模块间的耦合。 • 模块化设计提高代码的可复用性和维护性。

基础模块与通用模块的分层方式

这部分可依需求调整: • 基础模块:封装不与业务相关的模块,如分类、工具类等。理想状态下,这一层不需修改即可在其他项目中使用。 • 通用模块:主要是通用业务模块,如公用组件、通用UIButton、瀑布流、时间计算NSDate等。此层应体现与业务挂钩的通用逻辑;如果完全独立于业务逻辑,则放入基础模块。

业务模块

组件化除了技术层面,更难的是业务模块设计,需要考虑整个项目的划分、当前分层的合理性及未来扩展性。频繁改动的模块需要合理设计前端接口及与其他模块的交互。

通常项目模块间的关系大致如下图,就是我们在进行组件化之前的项目,给个层次中间的关系,这里写的模块可以理解是类之间的关系。

各个模块都或多或少有关系,模块间进行通讯(即类之间的方法调用),需要进行#import导入头文件,是一种比较强的耦合关系。

模块化的组件间耦合问题

为了实现模块间的低耦合,首先需要解决模块间的耦合关系。模块越独立,系统的可维护性和灵活性就越高。基本思路是建立一个通讯中间层,各模块通过该中间层进行通讯,避免直接联系。

通讯中间层的问题

  1. 与中间层的强耦合:中间层需要导入所有模块,才能允许模块间通讯。
  2. 中间层过于庞大:如果A模块只想与C模块通讯,中间层却包含ABCD所有代码,这不合理。
  3. 排列组合问题:中间层设计为AB、AC、AD、BC、BD、CD等组合,会使设计复杂化。

CTMediator 通讯中间框架介绍 框架地址:github.com/casatwy/CTM... 代码结构图:

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

基本思路

  1. 项目划分:先对项目进行合理的业务模块划分,不考虑模块间通讯。
  2. 独立Target层:每个模块有一个独立的Target层,作为模块声明和入口,类似于.h文件。
  3. 入口封装:Target层封装模块提供给外部的功能,如登录、注册等,并进行业务判断或容错处理。

代码实现思路 • 杜绝耦合:通过runtime机制将方法调用转换为字符串形式,以类名和方法名(字符串)进行调用,避免模块间直接导入。 • 错误处理:框架内部对参数如target和action进行容错处理,以减少手误风险。

增加分类 - 减少中间层与模块间的耦合

设计目的

  1. 防止框架意外变化:创建分类进行拓展,不直接修改CTMediator源码,类似于使用AFN等三方框架时,通过独立类管理和封装。
  2. 减少强耦合:为每个模块独立出一个类,分类中的方法与Target层声明的方法一一对应。

实现方式 • 举例:在登录模块中,将登录、注册、忘记密码等功能方法分别在Target层写好声明和实现,然后复制到分类。 • 外部调用:外部只需导入这个分类即可使用相应功能,类似于常规分类的使用效果。 • 完整模块结构:一个完整的模块包括 分类 --> Target层 ---> 模块源码层。

CTMediator代码和demo介绍

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

  1. 项目整体介绍 项目代码结构 代码运行起来,是一个tableView 我们以 present image 这个操作
  2. 这个功能是将 DemoModuleADetailViewController modal出来,并需要往这个控制器里传递一个UIImage,DemoModuleADetailViewController 以下简称为模块A 模块A 声明暴露了一个imageView,用于传值
less 复制代码
@interface DemoModuleADetailViewController : UIViewController
@property (nonatomic, strong, readonly) UILabel *valueLabel;
@property (nonatomic, strong, readonly) UIImageView *imageView;
@end

模块A 的target 层 Target_A类对应的方法及其实现为如下,主要是创建模块A控制器,解析传进来的参数解,并进行赋值控制器,并实现modal。

ini 复制代码
- (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.我的理解起名和如何定义字符串,只要项目内部约定好就行,大家都根据这规则就好。

  1. 第二步解决了方法名的问题,然后我们整体看下方法的调用,从cell的点击一步一步。 cell 点击,调用CTM分类方法

分类方法的实现 调用到CTM内部方法,进行类 和 方法名的转换,创建对象,内部应做了相应的注释,下一步对 performSelector: 进一步封装

ini 复制代码
- (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进一步封装,并返回方法调用方的对象。

ini 复制代码
- (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调用就结束了,大致的流程为

相关推荐
Magnetic_h13 小时前
【iOS】单例模式
笔记·学习·ui·ios·单例模式·objective-c
归辞...15 小时前
「iOS」——单例模式
ios·单例模式·cocoa
yanling202316 小时前
黑神话悟空mac可以玩吗
macos·ios·crossove·crossove24
归辞...18 小时前
「iOS」viewController的生命周期
ios·cocoa·xcode
crasowas1 天前
Flutter问题记录 - 适配Xcode 16和iOS 18
flutter·ios·xcode
2401_852403551 天前
Mac导入iPhone的照片怎么删除?快速方法讲解
macos·ios·iphone
SchneeDuan1 天前
iOS六大设计原则&&设计模式
ios·设计模式·cocoa·设计原则
JohnsonXin2 天前
【兼容性记录】video标签在 IOS 和 安卓中的问题
android·前端·css·ios·h5·兼容性
蒙娜丽宁2 天前
Go语言错误处理详解
ios·golang·go·xcode·go1.19
名字不要太长 像我这样就好2 天前
【iOS】push和pop、present和dismiss
学习·macos·ios·objective-c·cocoa