美文网首页
iOS - 组件化

iOS - 组件化

作者: 搬砖的crystal | 来源:发表于2021-03-05 14:39 被阅读0次

一、概述

1.概念

组件化就是将项目拆分为多个模块(组件),解除模块间的耦合,通过主项目把模块(组件)结合起来。

2.模块拆分、解藕

项目在进行组件化过程要进行模块拆分与解耦,一般拆分为基础模块(稳定的、不常更改的,如底层网络模块、文件数据库处理等模块)和业务模块。模块间只存在上下层依赖,即业务模块依赖基础模块,业务模块间尽可能不横向依赖。

3.优缺点
优点
  • 各模块可独立运行,减少编译时间
  • 提高代码复用性
  • 项目代码清晰,提高开发效率,降低维护成本
  • 各业务模块独立,增加团队协作效率
缺点
  • 增加开发人员学习成本
  • 代码冗余,中间代码多
  • 项目复杂度高

项目规模越大使用组件化越是利大于弊。

二、方案

模块之间要会通信,要相互调用。例如moduleA想调用moduleB的通常这样写:

#import "ModuleAViewController.h"
#import "ModuleBViewController.h"
@interface ModuleAViewController ()

@end

@implementation ModuleAViewController
- (void)viewDidLoad {
    [super viewDidLoad];
}
-(void)gotoModuleB{
    ModuleBViewController *bVc = [[ModuleBViewController alloc]init];
    bVc.name = @"AAA";
    [self.navigationController pushViewController:bVc animated:YES];
}
@end
慢慢的,各个模块直接相互依赖
可以增加一个中间件来进行接耦 但是,还是会有问题:
  • Mediator增加去转发组件间调用?
  • 一个模块只和Mediator通信,怎么知道另一个组件提供了哪些接口?
  • 模块和Mediator间互相依赖,怎么解除依赖?
1.casa的target-action

前2个问题,可以在Mediator中直接提供接口,调用对应模块的方法:

#import "Mediator.h"
#import "ModuleBComponent.h"
@implementation Mediator

+(UIViewController *)moduleBViewControllerWithName:(NSString *)name{
    return [ModuleBComponent moduleB_componentViewControllerWithName:name];
}

@end
#import "ModuleBComponent.h"
#import "ModuleBViewController.h"
@implementation ModuleBComponent

+(UIViewController *)moduleB_componentViewControllerWithName:(NSString *)name{
    ModuleBViewController *bVc = [[ModuleBViewController alloc]init];
    bVc.name = name;
    return bVc;
}

@end

moduleA调用的时候:

-(void)gotoModuleB{
    UIViewController *vc = [Mediator moduleBViewControllerWithName:@"AAA"];
    [self.navigationController pushViewController:vc animated:YES];
}

这样写依赖关系并没有解除,Mediator依赖了所有模块,而调用者又依赖了Mediator。
怎样让Mediator解除对各个模块的依赖呢,同时又能调用各个模块暴露出来的方法呢,可以使用OC的法宝runtime:

#import "Mediator.h"
@implementation Mediator

+(UIViewController *)moduleBViewControllerWithName:(NSString *)name{
    Class cls = NSClassFromString(@"ModuleBComponent");
     return [cls performSelector:NSSelectorFromString(@"moduleB_componentViewControllerWithName:") withObject:@{@"name":name}];
}
这样Mediator没有对各个组件有依赖了,结构图就变成这样:

既然可以使用runtime解耦消除依赖,为什么还有使用Mediator呢?组件间直接使用runtime调用接口也还是有问题的:

  • 调用者写代码没有提示,每次调用写很多无用代码
  • runtime方法的参数个数和类型限制,导致只能每个接口统一传一个NSDictionary,key和value不明确,需要单独维护
  • 编译层面不依赖其他组件,实际还是依赖了,直接调用,没有引入调用的组件时就crash了

使用Mediator后:

  • 调用者写代码时有提示了
  • 参数类型和个数无限制了,由Mediator去处理
  • Mediator可以统一处理,调用某个组件方法不存在时,可以做相应操作,让调用者和组件间没有耦合

到了这里就能解决耦合的问题:各组件不互相依赖,组件间调用只依赖中间件Mediator,Mediator不依赖其他组件。接下来可以优化写法:

  • Mediator中每个方法都要写runtime方法,格式确定,可以抽取出来
  • 每个组件对外方法都在Mediator写一变,组件一多Mediator类的长度是恐怖的

优化后就是casca方案,target-action对应第一点,target就是class,action就是selector,通过一些规则化动态调用。Category对应第二点,每个组件写一个Mediator的Category。

总结一下:组件通过中间件通信,中间件通过runtime接口解耦,通过target-actio简化写法,通过Category感官上分离组件接口代码。

2.蘑菇街url-block

回到Mediator最初的三个问题,蘑菇街是用另一种方法解决的:注册表的方式,用URL表示接口,在模块启动时注册模块提供的接口:

#import <Foundation/Foundation.h>
@interface Mediator : NSObject
typedef void (^componentBlock) (id param);
@property (nonatomic, strong) NSMutableDictionary *cache;
+ (instancetype)shareInstance;
/* 注册url block */
- (void)registerURLPattern:(NSString *)urlPattern toHandler:(componentBlock)blk;
/* 调用 */
- (void)openURL:(NSString *)url withParam:(id)param;
@end

#import "Mediator.h"
@implementation Mediator
+ (instancetype)shareInstance {
    static dispatch_once_t onceToken;
    static Mediator *instance;
    dispatch_once(&onceToken, ^{
        instance = [[Mediator alloc] init];
        instance.cache = [NSMutableDictionary dictionary];
    });
    return instance;
}
- (void)registerURLPattern:(NSString *)urlPattern toHandler:(componentBlock)blk {
    [self.cache setObject:blk forKey:urlPattern];
}
- (void)openURL:(NSString *)url withParam:(id)param {
    componentBlock blk = [self.cache objectForKey:url];
    if (blk) blk(param);
}

@end
#import "ModuleBComponent.h"
#import "ModuleBViewController.h"
#import "Mediator.h"
@implementation ModuleBComponent
+ (void)load {
    [[Mediator shareInstance] registerURLPattern:@"moduleB://name" toHandler:^(id param) {
        ModuleBViewController *bVc = [[ModuleBViewController alloc]init];
        bVc.name = param[@"name"];
        UIWindow *window =  [[[UIApplication sharedApplication] windows] objectAtIndex:0];
        UINavigationController *nav = (UINavigationController *)window.rootViewController;
        [nav pushViewController:bVc animated:YES];
    }];
}

@end

 url-block
 */
-(void)gotoModuleB{
    [[Mediator shareInstance] openURL:@"moduleB://name" withParam:@{@"name": @"AAA"}];
}

同样每个模块没有依赖,Mediator也不依赖其他组件,不过组件和调用者都依赖了Mediator。
各个组件初始化时向Mediator注册对外提供的接口,Mediator通过保存在内存的表去知道有哪些模块哪些接口,接口的形式是url-block。
抛开URL的远程调用和本地调用混在一起的问题,只用本地调用的情况,对于本地调用,URL只是一个表示组件的key,没有其他作用,这样有三个问题:

  • 需要管理各个组件里有什么URL接口可供调用,蘑菇街做了后台专门管理
  • 每个组件都需要初始化,内存里需要保存一份表,组件多了会有内存问题
  • 参数格式不明确,是个灵活的dictionary,也需要单独管理参数格式

组件化是不应该和URL扯上关系,因为组件对外提供接口主要是模块代码层面的调用,我们称之为本地调用,而URL主要用于app间通信,我们称之为远程调用。
两者混在一起的问题就是:如果是 URL 的形式,那组件对外提供接口时就要同时考虑本地调用和远程调用两种情况,而远程调用有个限制,传递的参数类型有限制,只能传能被字符串化的数据,或者说只能传能被转成 json 的数据,像 UIImage 这类对象是不行的,所以如果组件接口要考虑远程调用,这里的参数就不能是这类非常规对象,接口的定义就受限了。
远程调用是本地调用的子集,这里混在一起导致组件只能提供子集的功能,无法像方案1那样提供全集功能。

3.蘑菇街优化方案protocol-class

蘑菇街为了补全本地调用的功能,为组件多增加了另一种方案,就是通过protocol-class注册表的方式。
首先有一个新的中间件:

#import <Foundation/Foundation.h>

@interface ProtocolMediator : NSObject
@property (nonatomic, strong) NSMutableDictionary *protocolCache;
+ (instancetype)shareInstance;
- (void)registerProtocol:(Protocol *)proto forClass:(Class)cls ;
- (Class)classForProtocol:(Protocol *)proto ;
@end
#import "ProtocolMediator.h"

@implementation ProtocolMediator
+ (instancetype)shareInstance {
    static dispatch_once_t onceToken;
    static ProtocolMediator *instance;
    dispatch_once(&onceToken, ^{
        instance = [[ProtocolMediator alloc] init];
        instance.protocolCache = [NSMutableDictionary dictionary];
    });
    return instance;
}
- (void)registerProtocol:(Protocol *)proto forClass:(Class)cls {
    [self.protocolCache setObject:cls forKey:NSStringFromProtocol(proto)];
}

- (Class)classForProtocol:(Protocol *)proto {
    return self.protocolCache[NSStringFromProtocol(proto)];
}

@end

然后有一个公共的Protocol文件,定义了每一个组件对外提供的接口:

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@protocol ModuleBComponentProtocol <NSObject>
- (UIViewController *)moduleB_componentViewControllerWithName:(NSString *)name;
@end
@interface ComponentProtocol : NSObject

@end

在模块中实现接口,并在初始化时调用registerProtocol注册:

#import "ModuleBComponent.h"
#import "ModuleBViewController.h"
#import "ProtocolMediator.h"
#import "ComponentProtocol.h"
@implementation ModuleBComponent

+ (void)load{
    [[ProtocolMediator shareInstance] registerProtocol:@protocol(ModuleBComponentProtocol) forClass:[self class]];
}
- (UIViewController *)moduleB_componentViewControllerWithName:(NSString *)name{
    ModuleBViewController *bVc = [[ModuleBViewController alloc]init];
    bVc.name = name;
    return bVc;
}
@end

调用者:

/**
 protocol-class
 */
-(void)gotoModuleB{
    Class cls = [[ProtocolMediator shareInstance] classForProtocol:@protocol(ModuleBComponentProtocol)];
    id moduleBComponent = [[cls alloc] init];
    UIViewController *vc = [moduleBComponent moduleB_componentViewControllerWithName:@"AAA"];
    [self.navigationController pushViewController:vc animated:YES];
}

这个方案和前两个方案最大的不同就是,它不是直接通过Mediator调用组件方法,而是通过Mediator拿到组件对象,在自行去调用方法。结果就是组件方法分散在各个模块,没有统一的入口,也就没法做组件不存在时的统一处理。主要存在的问题就是分散调用导致耦合。

相关文章

网友评论

      本文标题:iOS - 组件化

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