組件化方案(傳送門:blog.cnbang.net/tech/3080/)
首先我覺(jué)得”組件”在這里不太合適丁恭,因?yàn)榘次依斫饨M件是指比較小的功能塊臀晃,這些組件不需要多少組件間通信誉结,沒(méi)什么依賴,也就不需要做什么其他處理,面向?qū)ο缶湍芨愣ㄡ场6@里提到的是較大粒度的業(yè)務(wù)功能,我們習(xí)慣稱為”模塊”鞭莽。為了方便表述坊秸,下面模塊和組件代表同一個(gè)意思,都是指較大粒度的業(yè)務(wù)模塊澎怒。
一個(gè) APP 有多個(gè)模塊褒搔,模塊之間會(huì)通信,互相調(diào)用喷面,例如微信讀書有 書籍詳情 想法列表 閱讀器 發(fā)現(xiàn)卡片 等等模塊星瘾,這些模塊會(huì)互相調(diào)用,例如 書籍詳情要調(diào)起閱讀器和想法列表惧辈,閱讀器要調(diào)起想法列表和書籍詳情琳状,等等,一般我們是怎樣調(diào)用呢咬像,以閱讀器為例算撮,會(huì)這樣寫:
#import "WRBookDetailViewController.h"
#import "WRReviewViewController.h"
@implementation WRReadingViewController
- (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
看起來(lái)挺好,這樣做簡(jiǎn)單明了县昂,沒(méi)有多余的東西肮柜,項(xiàng)目初期推薦這樣快速開(kāi)發(fā),但到了項(xiàng)目越來(lái)越龐大倒彰,這種方式會(huì)有什么問(wèn)題呢审洞?顯而易見(jiàn),每個(gè)模塊都離不開(kāi)其他模塊待讳,互相依賴粘在一起成為一坨:
這樣揉成一坨對(duì)測(cè)試/編譯/開(kāi)發(fā)效率/后續(xù)擴(kuò)展都有一些壞處芒澜,那怎么解開(kāi)這一坨呢。很簡(jiǎn)單创淡,按軟件工程的思路痴晦,下意識(shí)就會(huì)加一個(gè)中間層:
叫他 Mediator Manager Router 什么都行,反正就是負(fù)責(zé)轉(zhuǎn)發(fā)信息的中間層琳彩,暫且叫他 Mediator誊酌。
看起來(lái)順眼多了,但這里有幾個(gè)問(wèn)題:
Mediator 怎么去轉(zhuǎn)發(fā)組件間調(diào)用露乏?
一個(gè)模塊只跟 Mediator 通信碧浊,怎么知道另一個(gè)模塊提供了什么接口?
按上圖的畫法瘟仿,模塊和 Mediator 間互相依賴箱锐,怎樣破除這個(gè)依賴?
方案1
對(duì)于前兩個(gè)問(wèn)題劳较,最直接的反應(yīng)就是在 Mediator 直接提供接口驹止,調(diào)用對(duì)應(yīng)模塊的方法:
//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
//BookDetailComponent 組件
#import "Mediator.h"
#import "WRBookDetailViewController.h"
@implementation BookDetailComponent
+ (UIViewController *)detailViewController:(NSString *)bookId {
WRBookDetailViewController *detailVC = [[WRBookDetailViewController alloc] initWithBookId:bookId];
return detailVC;
}
@end
//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
然后在閱讀模塊里:
//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
這就是一開(kāi)始架構(gòu)圖的實(shí)現(xiàn)浩聋,看起來(lái)顯然這樣做并沒(méi)有什么好處,依賴關(guān)系并沒(méi)有解除幢哨,Mediator 依賴了所有模塊赡勘,而調(diào)用者又依賴 Mediator嫂便,最后還是一坨互相依賴捞镰,跟原來(lái)沒(méi)有 Mediator 的方案相比除了更麻煩點(diǎn)其他沒(méi)區(qū)別。
那怎么辦呢毙替。
怎樣讓Mediator解除對(duì)各個(gè)組件的依賴岸售,同時(shí)又能調(diào)到各個(gè)組件暴露出來(lái)的方法?對(duì)于OC有一個(gè)法寶可以做到厂画,就是runtime反射調(diào)用:
//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 沒(méi)有再對(duì)各個(gè)組件有依賴了凸丸,你看已經(jīng)不需要 #import 什么東西了,對(duì)應(yīng)的架構(gòu)圖就變成:
只有調(diào)用其他組件接口時(shí)才需要依賴 Mediator袱院,組件開(kāi)發(fā)者不需要知道 Mediator 的存在屎慢。
等等,既然用runtime就可以解耦取消依賴忽洛,那還要Mediator做什么腻惠?組件間調(diào)用時(shí)直接用runtime接口調(diào)不就行了,這樣就可以沒(méi)有任何依賴就完成調(diào)用:
//WRReadingViewController.m
@implementation WRReadingViewController
- (void)gotoReview:(NSString *)bookId {
Class cls = NSClassFromString(@"ReviewComponent");
UIViewController *reviewVC = [cls performSelector:NSSelectorFromString(@"reviewViewController:") withObject:@{@"bookId":bookId, @"type": @(1)}];
[self.navigationController pushViewController:reviewVC];
}
@end
這樣就完全解耦了欲虚,但這樣做的問(wèn)題是:
調(diào)用者寫起來(lái)很惡心集灌,代碼提示都沒(méi)有,每次調(diào)用寫一坨复哆。
runtime方法的參數(shù)個(gè)數(shù)和類型限制欣喧,導(dǎo)致只能每個(gè)接口都統(tǒng)一傳一個(gè)NSDictionary。這個(gè)NSDictionary里的key value是什么不明確梯找,需要找個(gè)地方寫文檔說(shuō)明和查看唆阿。
編譯器層面不依賴其他組件,實(shí)際上還是依賴了锈锤,直接在這里調(diào)用驯鳖,沒(méi)有引入調(diào)用的組件時(shí)就掛了
把它移到Mediator后:
調(diào)用者寫起來(lái)不惡心,代碼提示也有了牙咏。
參數(shù)類型和個(gè)數(shù)無(wú)限制臼隔,由 Mediator 去轉(zhuǎn)就行了,組件提供的還是一個(gè)NSDictionary參數(shù)的接口妄壶,但在Mediator 里可以提供任意類型和個(gè)數(shù)的參數(shù)摔握,像上面的例子顯式要求參數(shù)NSString *bookId和NSInteger type。
Mediator可以做統(tǒng)一處理丁寄,調(diào)用某個(gè)組件方法時(shí)如果某個(gè)組件不存在氨淌,可以做相應(yīng)操作泊愧,讓調(diào)用者與組件間沒(méi)有耦合。
到這里盛正,基本上能解決我們的問(wèn)題:各組件互不依賴删咱,組件間調(diào)用只依賴中間件Mediator,Mediator不依賴其他組件豪筝。接下來(lái)就是優(yōu)化這套寫法痰滋,有兩個(gè)優(yōu)化點(diǎn):
Mediator 每一個(gè)方法里都要寫 runtime 方法,格式是確定的续崖,這是可以抽取出來(lái)的敲街。
每個(gè)組件對(duì)外方法都要在 Mediator 寫一遍,組件一多 Mediator 類的長(zhǎng)度是恐怖的严望。
優(yōu)化后就成了 casa 的方案多艇,target-action 對(duì)應(yīng)第一點(diǎn),target就是class像吻,action就是selector峻黍,通過(guò)一些規(guī)則簡(jiǎn)化動(dòng)態(tài)調(diào)用。Category 對(duì)應(yīng)第二點(diǎn)拨匆,每個(gè)組件寫一個(gè) Mediator 的 Category姆涩,讓 Mediator 不至于太長(zhǎng)。這里有個(gè)demo
總結(jié)起來(lái)就是涮雷,組件通過(guò)中間件通信阵面,中間件通過(guò) runtime 接口解耦,通過(guò) target-action 簡(jiǎn)化寫法洪鸭,通過(guò) category 感官上分離組件接口代碼样刷。
方案2
回到 Mediator 最初的三個(gè)問(wèn)題,蘑菇街用的是另一種方式解決:注冊(cè)表的方式览爵,用URL表示接口置鼻,在模塊啟動(dòng)時(shí)注冊(cè)模塊提供的接口,一個(gè)簡(jiǎn)化的實(shí)現(xiàn):
//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
//BookDetailComponent 組件
#import "Mediator.h"
#import "WRBookDetailViewController.h"
+ (void)initComponent {
[[Mediator sharedInstance] registerURLPattern:@"weread://bookDetail" toHandler:^(NSDictionary *param) {
WRBookDetailViewController *detailVC = [[WRBookDetailViewController alloc] initWithBookId:param[@"bookId"]];
[[UIApplication sharedApplication].keyWindow.rootViewController.navigationController pushViewController:detailVC animated:YES];
}];
}
//WRReadingViewController.m 調(diào)用者
//ReadingViewController.m
#import "Mediator.h"
+ (void)gotoDetail:(NSString *)bookId {
[[Mediator sharedInstance] openURL:@"weread://bookDetail" withParam:@{@"bookId": bookId}];
}
這樣同樣做到每個(gè)模塊間沒(méi)有依賴蜓竹,Mediator 也不依賴其他組件箕母,不過(guò)這里不一樣的一點(diǎn)是組件本身和調(diào)用者都依賴了Mediator,不過(guò)這不是重點(diǎn)俱济,架構(gòu)圖還是跟方案1一樣嘶是。
各個(gè)組件初始化時(shí)向 Mediator 注冊(cè)對(duì)外提供的接口,Mediator 通過(guò)保存在內(nèi)存的表去知道有哪些模塊哪些接口蛛碌,接口的形式是URL->block聂喇。
這里拋開(kāi)URL的遠(yuǎn)程調(diào)用和本地調(diào)用混在一起導(dǎo)致的問(wèn)題,先說(shuō)只用于本地調(diào)用的情況,對(duì)于本地調(diào)用希太,URL只是一個(gè)表示組件的key克饶,沒(méi)有其他作用,這樣做有三個(gè)問(wèn)題:
需要有個(gè)地方列出各個(gè)組件里有什么 URL 接口可供調(diào)用誊辉。蘑菇街做了個(gè)后臺(tái)專門管理矾湃。
每個(gè)組件都需要初始化,內(nèi)存里需要保存一份表堕澄,組件多了會(huì)有內(nèi)存問(wèn)題邀跃。
參數(shù)的格式不明確,是個(gè)靈活的 dictionary奈偏,也需要有個(gè)地方可以查參數(shù)格式坞嘀。
第二點(diǎn)沒(méi)法解決躯护,第一點(diǎn)和第三點(diǎn)可以跟前面那個(gè)方案一樣惊来,在 Mediator 每個(gè)組件暴露方法的轉(zhuǎn)接口,然后使用起來(lái)就跟前面那種方式一樣了棺滞。
拋開(kāi)URL不說(shuō)裁蚁,這種方案跟方案1的共同思路就是:Mediator 不能直接去調(diào)用組件的方法,因?yàn)檫@樣會(huì)產(chǎn)生依賴继准,那我就要通過(guò)其他方法去調(diào)用枉证,也就是通過(guò) 字符串->方法 的映射去調(diào)用。runtime 接口的className + selectorName -> IMP是一種移必,注冊(cè)表的key -> block是一種室谚,而前一種是 OC 自帶的特性,后一種需要內(nèi)存維持一份注冊(cè)表崔泵,這是不必要的秒赤。
現(xiàn)在說(shuō)回 URL,組件化是不應(yīng)該跟 URL 扯上關(guān)系的憎瘸,因?yàn)榻M件對(duì)外提供的接口主要是模塊間代碼層面上的調(diào)用入篮,我們先稱為本地調(diào)用,而 URL 主要用于 APP 間通信幌甘,姑且稱為遠(yuǎn)程調(diào)用潮售。按常規(guī)思路者應(yīng)該是對(duì)于遠(yuǎn)程調(diào)用,再加個(gè)中間層轉(zhuǎn)發(fā)到本地調(diào)用锅风,讓這兩者分開(kāi)酥诽。那這里這兩者混在一起有什么問(wèn)題呢?
如果是 URL 的形式皱埠,那組件對(duì)外提供接口時(shí)就要同時(shí)考慮本地調(diào)用和遠(yuǎn)程調(diào)用兩種情況肮帐,而遠(yuǎn)程調(diào)用有個(gè)限制,傳遞的參數(shù)類型有限制漱逸,只能傳能被字符串化的數(shù)據(jù)泪姨,或者說(shuō)只能傳能被轉(zhuǎn)成 json 的數(shù)據(jù)游沿,像 UIImage 這類對(duì)象是不行的,所以如果組件接口要考慮遠(yuǎn)程調(diào)用肮砾,這里的參數(shù)就不能是這類非常規(guī)對(duì)象诀黍,接口的定義就受限了。
用理論的話來(lái)說(shuō)就是仗处,遠(yuǎn)程調(diào)用是本地調(diào)用的子集眯勾,這里混在一起導(dǎo)致組件只能提供子集功能,無(wú)法提供像方案1那樣提供全集功能婆誓。所以這個(gè)方案是天生有缺陷的吃环,對(duì)于遺漏的這部分功能,蘑菇街使用了另一種方案補(bǔ)全洋幻,請(qǐng)看方案3郁轻。
方案3
蘑菇街為了補(bǔ)全本地調(diào)用的功能,為組件多加了另一種方案文留,就是通過(guò) protocol-class 注冊(cè)表的方式好唯。首先有一個(gè)新的中間件:
//ProtocolMediator.m 新中間件
@implementation ProtocolMediator
@property (nonatomic, storng) NSMutableDictionary *protocolCache
- (void)registerProtocol:(Protocol *)proto forClass:(Class)cls {
NSMutableDictionary *protocolCache;
[protocolCache setObject:cls forKey:NSStringFromProtocol(proto)];
}
- (Class)classForProtocol:(Protocol *)proto {
return protocolCache[NSStringFromProtocol(proto)];
}
@end
然后有一個(gè)公共Protocol文件,定義了每一個(gè)組件對(duì)外提供的接口:
//ComponentProtocol.h
@protocol BookDetailComponentProtocol
- (UIViewController *)bookDetailController:(NSString *)bookId;
- (UIImage *)coverImageWithBookId:(NSString *)bookId;
@end
@protocol ReviewComponentProtocol
- (UIViewController *)ReviewController:(NSString *)bookId;
@end
再在模塊里實(shí)現(xiàn)這些接口燥翅,并在初始化時(shí)調(diào)用 registerProtocol 注冊(cè)骑篙。
//BookDetailComponent 組件
#import "ProtocolMediator.h"
#import "ComponentProtocol.h"
#import "WRBookDetailViewController.h"
+ (void)initComponent
{
[[ProtocolMediator sharedInstance] registerProtocol:@protocol(BookDetailComponentProtocol) forClass:[self class];
}
- (UIViewController *)bookDetailController:(NSString *)bookId {
WRBookDetailViewController *detailVC = [[WRBookDetailViewController alloc] initWithBookId:param[@"bookId"]];
return detailVC;
}
- (UIImage *)coverImageWithBookId:(NSString *)bookId {
….
}
最后調(diào)用者通過(guò) protocol 從 ProtocolMediator 拿到提供這些方法的 Class,再進(jìn)行調(diào)用:
//WRReadingViewController.m 調(diào)用者
//ReadingViewController.m
#import "ProtocolMediator.h"
#import "ComponentProtocol.h"
+ (void)gotoDetail:(NSString *)bookId {
Class cls = [[ProtocolMediator sharedInstance] classForProtocol:BookDetailComponentProtocol];
id bookDetailComponent = [[cls alloc] init];
UIViewController *vc = [bookDetailComponent bookDetailController:bookId];
[[UIApplication sharedApplication].keyWindow.rootViewController.navigationController pushViewController:vc animated:YES];
}
這種思路有點(diǎn)繞森书,這個(gè)方案跟剛才兩個(gè)最大的不同就是靶端,它不是直接通過(guò) Mediator 調(diào)用組件方法,而是通過(guò) Mediator 拿到組件對(duì)象凛膏,再自行去調(diào)用組件方法杨名。
結(jié)果就是組件方法的調(diào)用是分散在各地的,沒(méi)有統(tǒng)一的入口译柏,也就沒(méi)法做組件不存在時(shí)的統(tǒng)一處理镣煮。組件1調(diào)用了組件2的方法,如果用前面兩種方式鄙麦,組件間是沒(méi)有依賴的典唇,組件1+Mediator可以單獨(dú)抽離出來(lái),只需要在Mediator里做好調(diào)用組件2方法時(shí)的異常處理就行胯府。而這種方法組件1對(duì)組件2的調(diào)用分散在各個(gè)地方介衔,沒(méi)法做這些處理,在不修改組件1代碼的情況下骂因,組件1和組件2是分不開(kāi)的炎咖。
當(dāng)然你也可以在這上面跟方案1一樣在 Mediator 對(duì)每一個(gè)組件接口 wrapper 一層,那這樣這種方案跟方案1比除了更復(fù)雜點(diǎn),其他沒(méi)什么區(qū)別乘盼。
在 protocol-class 這個(gè)方案上升熊,主要存在的問(wèn)題就是分散調(diào)用導(dǎo)致耦合,另外實(shí)現(xiàn)上會(huì)有一些繞绸栅,其他就沒(méi)什么了级野。casa 說(shuō)的 “protocol對(duì)業(yè)務(wù)產(chǎn)生了侵入,且不符合黑盒模型粹胯”腿幔” 其實(shí)并沒(méi)有這么夸張,實(shí)際上 protocol 對(duì)外提供組件方法风纠,跟方案1在 Mediator wrapper 對(duì)外提供組件方法是差不多的况鸣。