最近在學(xué)習(xí)組件化的一些方案逗爹。這里收集消化了一下亡嫌,分享給大家參考。
組件化是架構(gòu)層面的一個(gè)概念掘而,它把項(xiàng)目按照某些規(guī)則(比如:按功能挟冠、按業(yè)務(wù))劃分成若干個(gè)顆粒度較小的單位,我們把這些單位稱之為組件袍睡,或者是模塊知染,來達(dá)到優(yōu)化項(xiàng)目結(jié)構(gòu)的目的。
組件又可以細(xì)分為 功能組件(如:圖片庫斑胜,網(wǎng)絡(luò)庫)控淡,業(yè)務(wù)組件也叫模塊(如:訂單模塊,個(gè)人中心模塊)
功能組件主要是物理層面的拆分止潘,方便以后的復(fù)用
業(yè)務(wù)組件強(qiáng)調(diào)邏輯拆分掺炭,以便解耦
組件化的發(fā)展歷程
開發(fā)之初,在功能方面凭戴,我們會(huì)把項(xiàng)目劃分為基礎(chǔ)層涧狮、網(wǎng)絡(luò)層、數(shù)據(jù)層等等么夫,而業(yè)務(wù)層面者冤,僅僅是按照目錄結(jié)構(gòu)做一個(gè)簡單的模塊分層,比如訂單模塊档痪、個(gè)人中心模塊
注:因?yàn)楣δ芙M件被大部分業(yè)務(wù)模塊所依賴涉枫,這里暫時(shí)不討論功能模塊的架構(gòu)問題。
隨著業(yè)務(wù)的發(fā)展腐螟,項(xiàng)目變的越來越復(fù)雜愿汰,APP
內(nèi)各個(gè)業(yè)務(wù)之間的耦合嚴(yán)重困后,邊界越來越模糊,經(jīng)常會(huì)出現(xiàn)你中有我尼桶,我中有你的情況操灿,如圖
可以看到锯仪,模塊與模塊嚴(yán)重耦合泵督,對(duì)代碼的擴(kuò)展,以及代碼的開發(fā)效率造成了巨大影響庶喜,有一種改一處動(dòng)全身的感覺小腊。發(fā)展到這個(gè)階段,我們會(huì)把各個(gè)模塊分割開來久窟,通過中介者來完成不同模塊之間的交互秩冈。如圖
這個(gè)時(shí)候,架構(gòu)看起來是清晰了許多斥扛。但因?yàn)橹薪檎呷稳环聪蛞蕾嚇I(yè)務(wù)模塊入问,任然存在改一處多個(gè)模塊受影響的情況,依賴仍舊是雙向的稀颁。我們舉個(gè)例子:
假如我現(xiàn)在在會(huì)員模塊芬失,要跳入到商品模塊除秀,此時(shí)活翩,會(huì)員模塊需要通過中介者來完成跳轉(zhuǎn)
// 會(huì)員模塊
[self.Intermediary gotoGoodsModuleWithParam:param]
// 在中介者中
- (void)gotoGoodsModuleWithParam:(id)param {
self.goodsModule.param = param; // 參數(shù)
[self.memberModule.navigationController pushViewController:self.goodsModule]
}
假如此時(shí)我們的需求變了谨娜,除了傳遞參數(shù)外萤衰,還需要額外傳一個(gè)參數(shù)來決定導(dǎo)航條的隱藏或者背景色阎肝,此時(shí)我們修改gotoGoodsModuleWithParam
函數(shù)的結(jié)構(gòu)颖系,那么就需要修改兩個(gè)地方凑阶,除了修改中介者中的方法以外稻据,還需要修改模塊中使用該函數(shù)的地方秃踩。
互相依賴其實(shí)是開發(fā)時(shí)候的大忌衬鱼,我們還要考慮模塊的循環(huán)引用問題、開發(fā)效率的問題憔杨、復(fù)用問題等等
如果消除中介者對(duì)模塊的依賴呢鸟赫?如圖:
是不是有那味道了。各模塊互不干涉芍秆,明確職責(zé)和邊界惯疙。
那么如何實(shí)現(xiàn)這種功能呢?下面來看看業(yè)內(nèi)的一些技術(shù)方案妖啥。
業(yè)內(nèi)組件化方案
- 基于路由URL的UI頁面統(tǒng)跳管理
- 基于反射的接口調(diào)用封裝
- 基于面向協(xié)議思想的服務(wù)注冊(cè)方案
- 基于通知的廣播方案
基于路由URL的UI頁面統(tǒng)跳管理
一般用法
// kRouteGoodsDetail = @"/goods/goods_detail"
UIViewController * vc = [Router handleURL:kRouteGoodsDetail];
if (vc) [self.navigationController pushViewController:vc animated:YES];
傳參的情況
//kRouteGoodsDetails = @“//goods/goods_detail?goods_id=%d”
NSString *urlStr = [NSString stringWithFormat:kRouteGoodsDetails, 123];
UIViewController *vc = [Router handleURL:urlStr];
if(vc) {
[self.navigationController pushViewController:vc animated:YES];
}
復(fù)雜的參數(shù)類型
+ (nullable id)handleURL:(nonnull NSString *)urlStr
complexParams:(nullable NSDictionary*)complexParams
completion:(nullable RouteCompletion)completion;
從接口上可以看出來霉颠,通過@"/goods/goods_detail"
字符串,我們可以拿到需要交互的模塊荆虱,來進(jìn)行操作蒿偎。大家可以參考Bifrost朽们,有一定知識(shí)儲(chǔ)備的話,看起來會(huì)比較容易诉位。當(dāng)然這只是其中一種思路骑脱。
在Demo
中,作者把各個(gè)模塊以子項(xiàng)目的形式匯聚在一個(gè)主工程里苍糠,在需要路由服務(wù)的頁面叁丧,通過bindURL
的形式,完成字符串
與路由
的綁定岳瞭。
但是:路由的綁定放到load
方法中拥娄,會(huì)對(duì)項(xiàng)目的冷啟動(dòng)有一定的影響,而且瞳筏,把所有的業(yè)務(wù)功能組件綁定勢(shì)必造成不必要的內(nèi)存常駐稚瘾。
當(dāng)然,有贊團(tuán)隊(duì)進(jìn)行了很多的測(cè)試和思考姚炕,最終方案的敲定肯定是值得信賴的摊欠。
基于反射的接口調(diào)用封裝
大家知道OC是支持反射的,比如:
Class className = NSClassFromString(@"Person");
SEL sel = NSSelectorFromString(@"getPersonName:");
...
然后可以通過- (id)performSelector:(SEL)aSelector
來完成消息的發(fā)送柱宦。
但是這種方式存在大量的硬編碼些椒,也無法觸發(fā)編譯器的自動(dòng)補(bǔ)全,同時(shí)捷沸,只有在運(yùn)行時(shí)才可以發(fā)現(xiàn)一些未知的錯(cuò)誤摊沉。除此之外,無法實(shí)現(xiàn)多參數(shù)傳值和方法返回值的獲取的問題痒给。所以這里選擇NSInvocation
更合適
這里可以看下業(yè)界的 CTMediator 開源庫说墨。是基于Mediator
模式和Target-Action
模式來完成的
先說調(diào)用方法:(住:本次只討論本地模塊之間的調(diào)用苍柏,不考慮遠(yuǎn)程調(diào)用)
本地組件A在某處調(diào)用[[CTMediator sharedInstance] performTarget:targetName action:actionName params:@{...}]
向CTMediator
發(fā)起跨組件調(diào)用尼斧,CTMediator
根據(jù)獲得的target和action信息
,通過objective-C
的runtime
轉(zhuǎn)化生成target
實(shí)例以及對(duì)應(yīng)的action
選擇項(xiàng)试吁,然后最終調(diào)用到目標(biāo)業(yè)務(wù)提供的邏輯棺棵,完成需求。
組件僅需要通過Action
暴露可調(diào)用的接口即可熄捍。所有組件都通過組件自帶的Target-Action
來響應(yīng)烛恤,也就是說,模塊與模塊之間的接口被固化在了Target-Action
這一層余耽,避免了實(shí)施組件化的改造過程中缚柏,對(duì)Business
的侵入,同時(shí)也提高了組件化接口的可維護(hù)性碟贾。
最后币喧,調(diào)用者通過響應(yīng)者給 CTMediator
做的 category
或者 extension
來發(fā)起調(diào)用轨域,來避免使用字符串調(diào)用出現(xiàn)的不友好。
代碼大家可以看下杀餐,可以說非常簡潔了干发。考慮的也非常全面史翘。關(guān)于一些架構(gòu)的思想可以看下大佬casatwy
的文章iOS應(yīng)用架構(gòu)談 組件化方案枉长。
簡化下代碼如下:
// Mediator提供基于NSInvocation的接口調(diào)用方法的統(tǒng)一入口
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params;
// 業(yè)務(wù)模塊對(duì)外提供的方法封裝到Category中
@interface CTMediator (Goods)
- (NSArray *)goods_getGoodsList;
- (void)goods_gotoGoodsDetail:(NSString *)id;
...
@end
@impletation CTMediator(Goods)
- (NSArray *)goods_getGoodsList{
return [self performTarget:@“Target_Goods” action:@"getGoodsList" params:nil];
}
- (void *)goods_gotoGoodsDetail:(NSString *)id{
return [self performTarget:@“Target_Goods” action:@"gotoGoodsDetail" params:{@"id":id}];
}
@interface Target_Goods : NSObject
- (NSArray *)getGoodsList;
- (void)gotoGoodsDetail:(NSString *)id;
@end
基于面向協(xié)議思想的服務(wù)注冊(cè)方案
每個(gè)模塊提供自己的服務(wù)協(xié)議,然后將此協(xié)議聲明注冊(cè)到中間層恶座。調(diào)用方能從中間層看到有哪些服務(wù)的接口搀暑,然后直接調(diào)用即可。
// 在中間件中完成協(xié)議的聲明跨琳,方便所有模塊調(diào)用和查閱
@protocol GoodsModuleService
- (NSArray*)getGoodsList;
- (NSInteger)getGoodsCount;
...
@end
// 在模塊的load方法中,注冊(cè)協(xié)議桐罕。并且讓該模塊實(shí)現(xiàn)協(xié)議中的方法
@interface GoodsModule : NSObject<GoodsModuleService>
@end
@implementation GoodsModule
+ (void)load {
[ServiceManager registerService:@protocol(GoodsModuleService) withModule:self.class]
}
//提供具體實(shí)現(xiàn)
- (NSArray*)getGoodsList {...}
- (NSInteger)getGoodsCount {...}
// 在其他模塊中調(diào)用
id<GoodsModuleService> goodsModule = [ServiceManager objByService:@protocol(GoodsModuleService)];
NSArray *list = [goodsModule getGoodsList];
...
面向協(xié)議編程脉让,看起來是很酷的。而且也不需要寫反射代碼功炮。
但是:把協(xié)議的內(nèi)容放到公共的地方溅潜,一旦發(fā)生改變,意味著用到協(xié)議的地方都要改一遍薪伏,而且滚澜,load
方法中進(jìn)行協(xié)議綁定,還是會(huì)有老毛病存在嫁怀。
有贊團(tuán)隊(duì)设捐,對(duì)基于服務(wù)注冊(cè)所消耗的啟動(dòng)時(shí)間做了測(cè)試,答案是影響可以忽略不計(jì)塘淑。
通知廣播方案
基于通知的模塊間通訊的方案萝招,實(shí)現(xiàn)起來是最簡單的,直接基于系統(tǒng)提供的NSNotificationCenter
即可存捺。適合一對(duì)多的通訊場(chǎng)景槐沼。但是劣勢(shì)也特別明顯。復(fù)雜的數(shù)據(jù)傳輸捌治,同步調(diào)用等方式都不太方便岗钩。通常用來作為以上幾種方案的補(bǔ)充。
總結(jié)
組件化是項(xiàng)目發(fā)展到一定程度后的一種選擇肖油。如果不考慮時(shí)間成本和技術(shù)成本的話兼吓,是可以去搞一下的。雖然一開始的技術(shù)壁壘較高构韵,但是對(duì)后續(xù)的迭代和升級(jí)是非常有幫助的周蹭。
多提一句趋艘。我們?cè)谧黾軜?gòu)的時(shí)候,一定是基于自己的實(shí)際項(xiàng)目來搞的凶朗,業(yè)界很多優(yōu)秀的思想可以拿來參考瓷胧,沒有必要生搬硬套。因?yàn)椴]有絕對(duì)正確的架構(gòu)棚愤,只有最適合自己的架構(gòu)搓萧。
以上就是對(duì)組件化的一些整理和思考,希望對(duì)大家有幫助宛畦。