一、概述
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拿到组件对象,在自行去调用方法。结果就是组件方法分散在各个模块,没有统一的入口,也就没法做组件不存在时的统一处理。主要存在的问题就是分散调用导致耦合。
网友评论