iOS 组件化

看完 Limboy(蘑菇街 App 的组件化之路) 和 Casa(iOS应用架构谈 组件化方案) 关于 iOS 上模块化的讨论,以及 Bang(iOS 组件化方案探索)的总结,整理一下。

什么是组件化

将程序中功能相对独立的部分打包在一起形成模块,并且减少模块之间的直接依赖。

组件化

可以看到,组件化是在横向分层的基础上,纵向对业务层进行切割。

底层是中间件(业务无关的组件,如网络请求、存储、工具类),共同为上层业务提供服务,允许相互引用(但应尽量减少);业务组件在上层依赖中间件,相互之间不允许直接引用。

一个简单的验证方法是单独的业务模块配合中间件能不能独立编译通过。

为什么要组件化

组件化是相对于原始的单一工程开发模式而言的。在业务初期,采用单一工程开发周期更短,是合适的开发模式,但是随着业务复杂度不断增加,工程越来越庞大,开发人员逐渐增多,单一工程的开发模式会出现一系列问题:

  • 耦合严重:组件之间依赖过于复杂,维护成本高。
  • 容易出现冲突:xib 或者代码冲突机会大大增加。
  • 开发效率低:每次都需要编译整个项目。

耦合问题严重

总的来说,组件化最大的好处就是低耦合,这是程序开发最最基本的原则,可以带来诸多好处:

  • 维护成本更低。
  • 复用性更好。
  • 版本控制冲突更小。

当然组件化也有一些代价:

  • 对新加入的开发人员不友好。
  • 开发流程更复杂,开发成本更高。
  • 模块化初期投入较大,影响发版节奏。

因此需要根据业务所处的阶段来进行取舍。

方案

对于中间件,直接封装成 pod 放在私有的 repo 上即可;对于业务组件,重点是如何解决引用问题。

问题是本质是组件之间服务的提供和调用。为此我们需要引入一个中间人(mediator、router)来传递这些信息:

中间人

这样一来各个业务组件只需要依赖中间人。方案需要解决两个问题:

  • 服务的发现。
  • 服务的调用,并且避免中间人对组件的依赖。

服务提供

接口方式

在 mediator 上直接提供接口来调用对应组件的方法。为了避免类过于臃肿,采用 category 的方式进行优化。

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

优点:

  • 服务信息比较直观。
  • 有代码补全和参数信息。

缺点:

  • Mediator 需要频繁改动。

URL 方式

用 URL 表示接口,URL 在模块初始化时注册。

1
2
3
4
5
6
7
8
9
10
11
12
13
//Mediator.m 中间件
@implementation Mediator
typedef void (^componentBlock) (id param);
@property (nonatomic, storng) NSMutableDictionary *cache
- (void)registerURLPattern:(NSString *)urlPattern toHandler:(componentBlock)blk {
[cache setObject:blk forKey:urlPattern];
}

- (void)openURL:(NSString *)url withParam:(id)param {
componentBlock blk = [cache objectForKey:url];
if (blk) blk(param);
}
@end
1
2
3
4
5
6
7
//WRReadingViewController.m 调用者
//ReadingViewController.m
#import "Mediator.h"

+ (void)gotoDetail:(NSString *)bookId {
[[Mediator sharedInstance] openURL:@"weread://bookDetail" withParam:@{@"bookId": bookId}];
}

优点:

  • 可以灵活配置,甚至从服务器下发动态修改行为。
  • Android 和 iOS 可以统一。

缺点:

  • 服务信息无法从代码层面获取,需要通过其他渠道(后台系统、文档)治理。

注意点:

外部调用(URLScheme)应该和内部调用区分开:

  • 外部调用无法传递复杂参数(如 UIImage),内外调用行为无法统一。
  • 外部调用是内部调用的子集,或者说外部调用应该基于内部调用暴露出去。
  • 不作区分会有安全隐患,可能存在通过外部调用到无权限服务的情况。

服务调用

Runtime 方式

调用服务时,mediator 通过 Objective-C 的 runtime 来实例化组件,避免对组件的依赖。

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

这个方法存在的一个小问题是 mediator 实现中存在一些硬编码。

Block 方式

在 mediator 中维护一个 URL 和 block 的映射,因此不需要依赖组件。

不足之处是映射表需要占用一定的内存。

业务组件内部

对于不同的业务组件内部,可以使用 MVP、MVVM、VIPER 等架构,需要研发团队协商并建立规范。

延伸阅读

给鸡排饭加个蛋