http://wereadteam.github.io/2016/03/19/iOS-Component/
看了 Limboy(文章1?文章2) 和 Casa (文章) 對(duì) iOS 組件化方案的討論谚中,寫(xiě)篇文章梳理下思路建瘫。
首先我覺(jué)得”組件”在這里不太合適,因?yàn)榘次依斫饨M件是指比較小的功能塊吏廉,這些組件不需要多少組件間通信笋庄,沒(méi)什么依賴(lài)效扫,也就不需要做什么其他處理,面向?qū)ο缶湍芨愣ㄖ鄙啊6@里提到的是較大粒度的業(yè)務(wù)功能菌仁,我們習(xí)慣稱(chēng)為”模塊”。為了方便表述静暂,下面模塊和組件代表同一個(gè)意思济丘,都是指較大粒度的業(yè)務(wù)模塊。
一個(gè) APP 有多個(gè)模塊洽蛀,模塊之間會(huì)通信摹迷,互相調(diào)用,例如微信讀書(shū)有 書(shū)籍詳情 想法列表 閱讀器 發(fā)現(xiàn)卡片 等等模塊郊供,這些模塊會(huì)互相調(diào)用峡碉,例如 書(shū)籍詳情要調(diào)起閱讀器和想法列表,閱讀器要調(diào)起想法列表和書(shū)籍詳情颂碘,等等异赫,一般我們是怎樣調(diào)用呢椅挣,以閱讀器為例,會(huì)這樣寫(xiě):
1
2
3
4
5
6
7
8
9
10
11
12
13
#import"WRBookDetailViewController.h"
#import"WRReviewViewController.h"
@implementationMediator
- (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)其他模塊荠列,互相依賴(là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è)模塊提供了什么接口煞聪?
按上圖的畫(huà)法斗躏,模塊和 Mediator 間互相依賴(lài),怎樣破除這個(gè)依賴(lài)昔脯?
對(duì)于前兩個(gè)問(wèn)題啄糙,最直接的反應(yīng)就是在 Mediator 直接提供接口,調(diào)用對(duì)應(yīng)模塊的方法:
1
2
3
4
5
6
7
8
9
10
11
//Mediator.m
#import"BookDetailComponent.h"
#import"ReviewComponent.h"
@implementationMediator
+ (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"
@implementationBookDetailComponent
+ (UIViewController*)detailViewController:(NSString*)bookId {
? ? WRBookDetailViewController *detailVC = [[WRBookDetailViewController alloc] initWithBookId:bookId];
returndetailVC;
}
@end
1
2
3
4
5
6
7
8
9
//ReviewComponent? 組件
#import"Mediator.h"
#import"WRReviewViewController.h"
@implementationReviewComponent
+ (UIViewController*)reviewViewController:(NSString*)bookId type:(NSInteger)type {
UIViewController*reviewVC = [[WRReviewViewController alloc] initWithBookId:bookId type:type];
returnreviewVC;
}
@end
然后在閱讀模塊里:
1
2
3
4
5
6
7
8
9
10
11
//WRReadingViewController.m
#import"Mediator.h"
@implementationWRReadingViewController
- (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)有什么好處迈套,依賴(lài)關(guān)系并沒(méi)有解除,Mediator 依賴(lài)了所有模塊碱鳞,而調(diào)用者又依賴(lài) Mediator桑李,最后還是一坨互相依賴(lài),跟原來(lái)沒(méi)有 Mediator 的方案相比除了更麻煩點(diǎn)其他沒(méi)區(qū)別窿给。
那怎么辦呢贵白。
怎樣讓Mediator解除對(duì)各個(gè)組件的依賴(lài),同時(shí)又能調(diào)到各個(gè)組件暴露出來(lái)的方法崩泡?對(duì)于OC有一個(gè)法寶可以做到禁荒,就是runtime反射調(diào)用:
1
2
3
4
5
6
7
8
9
10
11
12
13
//Mediator.m
@implementationMediator
+ (UIViewController*)BookDetailComponent_viewController:(NSString*)bookId {
Class cls =NSClassFromString(@"BookDetailComponent");
idobj = [[cls alloc] init];
return[obj performSelector:NSSelectorFromString(@"detailViewController:") withObject:@{@"bookId":bookId}];
}
+ (UIViewController*)ReviewComponent_viewController:(NSString*)bookId type:(NSInteger)type {
Class cls =NSClassFromString(@"ReviewComponent");
idobj = [[cls alloc] init];
return[obj performSelector:NSSelectorFromString(@"reviewViewController:") withObject:@{@"bookId":bookId,@"type": @(type)}];
}
@end
這下 Mediator 沒(méi)有再對(duì)各個(gè)組件有依賴(lài)了,你看已經(jīng)不需要 #import 什么東西了角撞,對(duì)應(yīng)的架構(gòu)圖就變成:
只有調(diào)用其他組件接口時(shí)才需要依賴(lài) Mediator呛伴,組件開(kāi)發(fā)者不需要知道 Mediator 的存在勃痴。
等等,既然用runtime就可以解耦取消依賴(lài)热康,那還要Mediator做什么沛申?組件間調(diào)用時(shí)直接用runtime接口調(diào)不就行了,這樣就可以沒(méi)有任何依賴(lài)就完成調(diào)用:
1
2
3
4
5
6
7
8
9
//WRReadingViewController.m
@implementationWRReadingViewController
- (void)gotoReview:(NSString*)bookId {
Class cls =NSClassFromString(@"BookDetailComponent");
idobj = [[cls alloc] init];
UIViewController*reviewVC = [obj performSelector:NSSelectorFromString(@"reviewViewController:") withObject:@{@"bookId":bookId,@"type": @(1)}];
[self.navigationController pushViewController:reviewVC];
}
@end
這樣就完全解耦了姐军,但這樣做的問(wèn)題是:
調(diào)用者寫(xiě)起來(lái)很惡心铁材,代碼提示都沒(méi)有,每次調(diào)用寫(xiě)一坨奕锌。
runtime方法的參數(shù)個(gè)數(shù)和類(lèi)型限制著觉,導(dǎo)致只能每個(gè)接口都統(tǒng)一傳一個(gè)?NSDictionary。這個(gè)?NSDictionary里的key value是什么不明確惊暴,需要找個(gè)地方寫(xiě)文檔說(shuō)明和查看饼丘。
編譯器層面不依賴(lài)其他組件,實(shí)際上還是依賴(lài)了辽话,直接在這里調(diào)用葬毫,沒(méi)有引入調(diào)用的組件時(shí)就掛了
把它移到Mediator后:
調(diào)用者寫(xiě)起來(lái)不惡心,代碼提示也有了屡穗。
參數(shù)類(lèi)型和個(gè)數(shù)無(wú)限制,由 Mediator 去轉(zhuǎn)就行了忽肛,組件提供的還是一個(gè)?NSDictionary?參數(shù)的接口村砂,但在Mediator 里可以提供任意類(lèi)型和個(gè)數(shù)的參數(shù),像上面的例子顯式要求參數(shù)?NSString *bookId?和?NSInteger type屹逛。
Mediator可以做統(tǒng)一處理础废,調(diào)用某個(gè)組件方法時(shí)如果某個(gè)組件不存在,可以做相應(yīng)操作罕模,讓調(diào)用者與組件間沒(méi)有耦合评腺。
到這里,基本上能解決我們的問(wèn)題:各組件互不依賴(lài)淑掌,組件間調(diào)用只依賴(lài)中間件Mediator蒿讥,Mediator不依賴(lài)其他組件。接下來(lái)就是優(yōu)化這套寫(xiě)法抛腕,有兩個(gè)優(yōu)化點(diǎn):
Mediator 每一個(gè)方法里都要寫(xiě) runtime 方法芋绸,格式是確定的,這是可以抽取出來(lái)的担敌。
每個(gè)組件對(duì)外方法都要在 Mediator 寫(xiě)一遍摔敛,組件一多 Mediator 類(lèi)的長(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è)組件寫(xiě)一個(gè) Mediator 的 Category,讓 Mediator 不至于太長(zhǎng)敢伸。這里有個(gè)demo扯饶。
總結(jié)起來(lái)就是,組件通過(guò)中間件通信池颈,中間件通過(guò) runtime 接口解耦尾序,通過(guò) target-action 簡(jiǎn)化寫(xiě)法,通過(guò) category 感官上分離組件接口代碼躯砰。這里可以看到這個(gè)實(shí)現(xiàn)的?Demo每币。
回到 Mediator 最初的三個(gè)問(wèn)題,蘑菇街用的是另一種方式解決:注冊(cè)表的方式琢歇,用URL表示接口兰怠,在模塊啟動(dòng)時(shí)注冊(cè)模塊提供的接口,一個(gè)簡(jiǎn)化的實(shí)現(xiàn):
1
2
3
4
5
6
7
8
9
10
11
12
13
//Mediator.m? 中間件
@implementationMediator
typedefvoid(^componentBlock) (idparam);
@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(bulk) blk(param);
}
@end
1
2
3
4
5
6
7
8
9
//BookDetailComponent? 組件
#import"Mediator.h"
#import"WRBookDetailViewController.h"
+ (void)initComponent {
[[Mediator sharedInstance] registerURLPattern:@"weread://bookDetail"toHandler:^(NSDictionary*param) {
WRBookDetailViewController *detailVC = [[WRBookDetailViewController alloc] initWithBookId:param[@"bookId"]];
[[UIApplicationsharedApplication].keyWindow.rootViewController.navigationController pushViewController:detailVC animated:YES];
? }];
}
1
2
3
4
5
6
7
//WRReadingViewController.m? 調(diào)用者
//ReadingViewController.m
#import"Mediator.h"
+ (void)gotoDetail:(NSString*)bookId {
[[Mediator sharedInstance] openURL:@"weread://bookDetail"withParam:@{@"bookId": bookId}];
}
這樣同樣做到每個(gè)模塊間沒(méi)有依賴(lài)李茫,Mediator 也不依賴(lài)其他組件揭保,不過(guò)這里不一樣的一點(diǎn)是組件本身和調(diào)用者都依賴(lài)了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)專(zhuān)門(mén)管理。
每個(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)生依賴(lài),那我就要通過(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)用扔嵌,我們先稱(chēng)為本地調(diào)用限府,而 URL 主要用于 APP 間通信,姑且稱(chēng)為遠(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ù)類(lèi)型有限制抚恒,只能傳能被字符串化的數(shù)據(jù)咱台,或者說(shuō)只能傳能被轉(zhuǎn)成 json 的數(shù)據(jù),像 UIImage 這類(lèi)對(duì)象是不行的俭驮,所以如果組件接口要考慮遠(yuǎn)程調(diào)用回溺,這里的參數(shù)就不能是這類(lèi)非常規(guī)對(duì)象,接口的定義就受限了混萝。
用理論的話(huà)來(lái)說(shuō)就是遗遵,遠(yuǎn)程調(diào)用是本地調(diào)用的子集,這里混在一起導(dǎo)致組件只能提供子集功能逸嘀,無(wú)法提供像方案1那樣提供全集功能车要。所以這個(gè)方案是天生有缺陷的,對(duì)于遺漏的這部分功能崭倘,蘑菇街使用了另一種方案補(bǔ)全翼岁,請(qǐng)看方案3类垫。
蘑菇街為了補(bǔ)全本地調(diào)用的功能,為組件多加了另一種方案琅坡,就是通過(guò) protocol-class 注冊(cè)表的方式悉患。首先有一個(gè)新的中間件:
1
2
3
4
5
6
7
8
9
10
11
12
//ProtocolMediator.m? 新中間件
@implementationProtocolMediator
@property(nonatomic, storng)NSMutableDictionary*protocolCache
- (void)registerProtocol:(Protocol *)proto forClass:(Class)cls {
NSMutableDictionary*protocolCache;
[protocolCache setObject:cls forKey:NSStringFromProtocol(proto)];
}
- (Class)classForProtocol:(Protocol *)proto {
returnprotocolCache[NSStringFromProtocol(proto)];
}
@end
然后有一個(gè)公共Protocol文件,定義了每一個(gè)組件對(duì)外提供的接口:
1
2
3
4
5
6
7
8
9
//ComponentProtocol.h
@protocolBookDetailComponentProtocol
- (UIViewController*)bookDetailController:(NSString*)bookId;
- (UIImage*)coverImageWithBookId:(NSString*)bookId;
@end
@protocolReviewComponentProtocol
- (UIViewController*)ReviewController:(NSString*)bookId;
@end
再在模塊里實(shí)現(xiàn)這些接口榆俺,并在初始化時(shí)調(diào)用 registerProtocol 注冊(cè)售躁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//BookDetailComponent? 組件
#import"ProtocolMediator.h"
#import"ComponentProtocol.h"
#import"WRBookDetailViewController.h"
+ (void)initComponent
{
[[ProtocolMediator sharedInstance] registerProtocol:@protocol(BookDetailComponentProtocol)forClass:[selfclass];
}
- (UIViewController*)bookDetailController:(NSString*)bookId {
WRBookDetailViewController *detailVC = [[WRBookDetailViewController alloc] initWithBookId:param[@"bookId"]];
returndetailVC;
}
- (UIImage*)coverImageWithBookId:(NSString*)bookId {
? ? ….
}
最后調(diào)用者通過(guò) protocol 從 ProtocolMediator 拿到提供這些方法的 Class,再進(jìn)行調(diào)用:
1
2
3
4
5
6
7
8
9
10
//WRReadingViewController.m? 調(diào)用者
//ReadingViewController.m
#import"ProtocolMediator.h"
#import"ComponentProtocol.h"
+ (void)gotoDetail:(NSString*)bookId {
? ? Class cls = [[ProtocolMediator sharedInstance] classForProtocol:BookDetailComponentProtocol];
idbookDetailComponent = [[cls alloc] init];
UIViewController*vc = [bookDetailComponent bookDetailController:bookId];
[[UIApplicationsharedApplication].keyWindow.rootViewController.navigationController.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)有依賴(là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ì)外提供組件方法是差不多的。
蘑菇街在一個(gè)項(xiàng)目里同時(shí)用了方案2和方案3兩種方式,會(huì)讓寫(xiě)組件的人不知所措怨喘,新增一個(gè)接口時(shí)不知道該用方案2的方式還是方案3的方式津畸,可能這個(gè)在蘑菇街內(nèi)部會(huì)通過(guò)一些文檔規(guī)則去規(guī)范,但其實(shí)是沒(méi)有必要的必怜∪馔兀可能是蘑菇街作為電商平臺(tái)一開(kāi)始就注重APP頁(yè)面間跳轉(zhuǎn)的概念,每個(gè)模塊已經(jīng)有一個(gè)對(duì)應(yīng)的URL梳庆,于是組件化時(shí)自然想到通過(guò)URL的方式表示組件暖途,后續(xù)發(fā)現(xiàn)URL方式的限制,于是加上方案3的方式膏执,這也是正常的探索過(guò)程驻售。
上面論述下方案1確實(shí)比方案2+方案3簡(jiǎn)單明了,沒(méi)有 注冊(cè)表常駐內(nèi)存/參數(shù)傳遞限制/調(diào)用分散 這些缺點(diǎn)更米,方案1多做的一步是需要對(duì)所有組件方法進(jìn)行一層 wrapper欺栗,但若想要明確提供組件的方法和參數(shù)類(lèi)型,解耦統(tǒng)一處理征峦,方案2和方案3同樣需要多加這層迟几。
實(shí)際上我沒(méi)有組件化相關(guān)的實(shí)踐,這里僅從 limboy 和 casa 提供的這幾個(gè)方案對(duì)比分析栏笆,我還對(duì)組件化帶來(lái)的收益是否大于組件化增加的成本這點(diǎn)存疑类腮,相信真正實(shí)踐起來(lái)還會(huì)碰到很多坑,繼續(xù)探索中蛉加。