学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

iOS组件化

前言

查了下资料,没找到关于组建化合理的定义,可以理解为模块化,对较大粒度的业务模块进行封装,组件间只有很少的依赖,只关注输入与输出。

问题

通常一个APP会有多个模块,模块之间会有通信,互相调用,例如微信读书有 书籍详情 想法列表 阅读器 发现卡片 等等模块,这些模块会互相调用,例如 书籍详情要调起阅读器和想法列表,阅读器要调起想法列表和书籍详情,等等,一般我们是怎样调用呢,以阅读器为例,会这样写:

1
2
3
4
5
6
7
8
9
10
11
12
#import "WRBookDetailViewController.h"
#import "WRReviewViewController.h"
@implementation Mediator
- (void)gotoDetail {
WRBookDetailViewController *detailVC = [[WRBookDetailViewController alloc] initWithBookId:self.bookId];
[self.navigationController.pushViewController:detailVC animated:YES];
}
- (void)gotoReview {
WRReviewViewController *reviewVC = [[WRReviewViewController alloc] initWithBookId:self.bookId reviewType:1];
[self.navigationController.pushViewController:reviewVC animated:YES];
}
@end

看起来挺好,这样做简单明了,没有多余的东西,项目初期推荐这样快速开发,但到了项目越来越庞大,这种方式会有什么问题呢?显而易见,每个模块都离不开其他模块,互相依赖粘在一起成为一坨:

这样揉成一坨对测试/编译/开发效率/后续扩展都有一些坏处,那怎么解开这一坨呢。很简单,按软件工程的思路,下意识就会加一个中间层:

负责转发信息的中间层,暂且叫他 Mediator(Mediator Manager Router)。

看起来顺眼多了,但这里有几个问题:

Mediator 怎么去转发组件间调用?
一个模块只跟 Mediator 通信,怎么知道另一个模块提供了什么接口?
按上图的画法,模块和 Mediator 间互相依赖,怎样破除这个依赖?

解耦

对于前两个问题,最直接的反应就是在 Mediator 直接提供接口,调用对应模块的方法:

1
2
3
4
5
6
7
8
9
10
11
//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
3
4
5
6
7
8
9
//BookDetailComponent  组件
#import "Mediator.h"
#import "WRBookDetailViewController.h"
@implementation BookDetailComponent
+ (UIViewController *)detailViewController:(NSString *)bookId {
WRBookDetailViewController *detailVC = [[WRBookDetailViewController alloc] initWithBookId:bookId];
return detailVC;
}
@end
1
2
3
4
5
6
7
8
9
//ReviewComponent  组件
#import "Mediator.h"
#import "WRReviewViewController.h"
@implementation ReviewComponent
+ (UIViewController *)reviewViewController:(NSString *)bookId type:(NSInteger)type {
UIViewController *reviewVC = [[WRReviewViewController alloc] initWithBookId:bookId type:type];
return reviewVC;
}
@end

然后在阅读模块里:

1
2
3
4
5
6
7
8
9
10
//WRReadingViewController.m
#import "Mediator.h"
@implementation WRReadingViewController
- (void)gotoDetail:(NSString *)bookId {
UIViewController *detailVC = [Mediator BookDetailComponent_viewControllerForDetail:bookId];
[self.navigationController pushViewController:detailVC];
UIViewController *reviewVC = [Mediator ReviewComponent_viewController:bookId type:1];
[self.navigationController pushViewController:reviewVC];
}
@end

问题来了,
依赖关系并没有解除,Mediator 依赖了所有模块,而调用者又依赖 Mediator,最后还是一坨互相依赖,跟原来没有 Mediator 的方案相比除了更麻烦点其他没区别。

怎样让Mediator解除对各个组件的依赖,同时又能调到各个组件暴露出来的方法?对于OC有一个法宝可以做到,就是runtime反射调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
//Mediator.m
@implementation Mediator
+ (UIViewController *)BookDetailComponent_viewController:(NSString *)bookId {
Class cls = NSClassFromString(@"BookDetailComponent");
id obj = [[cls alloc] init];
return [obj performSelector:NSSelectorFromString(@"detailViewController:") withObject:@{@"bookId":bookId}];
}
+ (UIViewController *)ReviewComponent_viewController:(NSString *)bookId type:(NSInteger)type {
Class cls = NSClassFromString(@"ReviewComponent");
id obj = [[cls alloc] init];
return [obj performSelector:NSSelectorFromString(@"reviewViewController:") withObject:@{@"bookId":bookId, @"type": @(type)}];
}
@end

这下 Mediator 没有再对各个组件有依赖了,你看已经不需要 #import 什么东西了,对应的架构图就变成:

只有调用其他组件接口时才需要依赖 Mediator,组件开发者不需要知道 Mediator 的存在。

既然用runtime就可以解耦取消依赖,那还要Mediator做什么?组件间调用时直接用runtime接口调不就行了,这样就可以没有任何依赖就完成调用

1
2
3
4
5
6
7
8
9
//WRReadingViewController.m
@implementation WRReadingViewController
- (void)gotoReview:(NSString *)bookId {
Class cls = NSClassFromString(@"BookDetailComponent");
id obj = [[cls alloc] init];
UIViewController *reviewVC = [obj performSelector:NSSelectorFromString(@"reviewViewController:") withObject:@{@"bookId":bookId, @"type": @(1)}];
[self.navigationController pushViewController:reviewVC];
}
@end

这样就完全解耦了,但这样做的问题是:

  1. 调用者写起来很恶心,代码提示都没有,每次调用写一坨。
  2. runtime方法的参数个数和类型限制,导致只能每个接口都统一传一个 NSDictionary。这个 NSDictionary里的key value是什么不明确,需要找个地方写文档说明和查看。
  3. 编译器层面不依赖其他组件,实际上还是依赖了,直接在这里调用,没有引入调用的组件时就挂了

把它移到Mediator后:

  1. 调用者写起来不恶心,代码提示也有了。
  2. 参数类型和个数无限制,由 Mediator 去转就行了,组件提供的还是一个 NSDictionary 参数的接口,但在Mediator 里可以提供任意类型和个数的参数,像上面的例子显式要求参数 NSString *bookId 和 NSInteger type。
  3. Mediator可以做统一处理,调用某个组件方法时如果某个组件不存在,可以做相应操作,让调用者与组件间没有耦合。

到这里,基本上能解决我们的问题:各组件互不依赖,组件间调用只依赖中间件Mediator,Mediator不依赖其他组件。接下来就是优化这套写法,有两个优化点:

  1. Mediator 每一个方法里都要写 runtime 方法,格式是确定的,这是可以抽取出来的。
  2. 每个组件对外方法都要在 Mediator 写一遍,组件一多 Mediator 类的长度是恐怖的。
    优化后就成了 casa 的方案,target-action 对应第一点,target就是class,action就是selector,通过一些规则简化动态调用。Category 对应第二点,每个组件写一个 Mediator 的 Category,让 Mediator 不至于太长。

总结起来就是,组件通过中间件通信,中间件通过 runtime 接口解耦,通过 target-action 简化写法,通过 category 感官上分离组件接口代码。这里可以看到这个实现的 Demo

文章参考

  1. iOS 组件化方案探索