有贊移動(dòng) iOS 組件化(模塊化)架構(gòu)設(shè)計(jì)實(shí)踐

一处面、背景

業(yè)務(wù)組件化(或者叫模塊化)作為移動(dòng)端應(yīng)用架構(gòu)的主流方式之一如贷,近年來(lái)一直是業(yè)界積極探索和實(shí)踐的方向陷虎。有贊移動(dòng)團(tuán)隊(duì)自16年起也在不斷嘗試各種組件化方案,在有贊微信商城杠袱,有贊零售尚猿,有贊美業(yè)等多個(gè)應(yīng)用中進(jìn)行了實(shí)踐。我們踩過(guò)一些坑楣富,也收獲了很多寶貴的經(jīng)驗(yàn)凿掂,并沉淀出 iOS 相關(guān)框架 Bifrost (雷神里的彩虹橋)。在過(guò)程中我們深刻體會(huì)到“沒(méi)有絕對(duì)正確的架構(gòu)纹蝴,只有最合適的架構(gòu)”這句話的意義庄萎。很多通用方案只是組件化的冰山一角,實(shí)際落地過(guò)程中還有相當(dāng)多的東西需要考量塘安。
本文并不準(zhǔn)備對(duì)組件化架構(gòu)設(shè)計(jì)方案給出一份標(biāo)準(zhǔn)答案惨恭,而是希望通過(guò)我們的實(shí)踐經(jīng)驗(yàn)和思考分析,提供一種思路耙旦,對(duì)遇到類似問(wèn)題的同學(xué)能有所啟發(fā)脱羡。

  1. 區(qū)別于功能模塊/組件(比如圖片庫(kù)萝究,網(wǎng)絡(luò)庫(kù)),本文討論的是業(yè)務(wù)模塊/組件(比如訂單模塊锉罐,商品模塊)相關(guān)的架構(gòu)設(shè)計(jì)帆竹。
  2. 相比組件(Component),個(gè)人感覺(jué)稱之為模塊(Module)更為合適脓规。組件強(qiáng)調(diào)物理拆分栽连,以便復(fù)用;模塊強(qiáng)調(diào)邏輯拆分侨舆,以便解耦秒紧。而且如果用過(guò) Android Studio, 會(huì)發(fā)現(xiàn)它創(chuàng)建的子系統(tǒng)都叫 Module. 但介于業(yè)界習(xí)慣稱之為組件化,所以我們繼續(xù)使用這個(gè)術(shù)語(yǔ)挨下。本文下面所用名詞熔恢,“模塊”等同于“組件”。

二臭笆、什么是業(yè)務(wù)模塊化(組件化)

傳統(tǒng)的 App 架構(gòu)設(shè)計(jì)更多強(qiáng)調(diào)的是分層叙淌,基于設(shè)計(jì)模式六大原則之一的單一職責(zé)原則,將系統(tǒng)劃分為基礎(chǔ)層愁铺,網(wǎng)絡(luò)層鹰霍,UI層等等,以便于維護(hù)和擴(kuò)展茵乱。但隨著業(yè)務(wù)的發(fā)展茂洒,系統(tǒng)變得越來(lái)越復(fù)雜,只做分層就不夠了瓶竭。App 內(nèi)各子系統(tǒng)之間耦合嚴(yán)重, 邊界越來(lái)越模糊督勺,經(jīng)常發(fā)生你中有我我中有你的情況(圖一)。這對(duì)代碼質(zhì)量在验,功能擴(kuò)展,以及開發(fā)效率都會(huì)造成很大的影響堵未。此時(shí)腋舌,一般會(huì)將各個(gè)子系統(tǒng)劃分為相對(duì)獨(dú)立的模塊,通過(guò)中介者模式收斂交互代碼渗蟹,把模塊間交互部分進(jìn)行集中封裝, 所有模塊間調(diào)用均通過(guò)中介者來(lái)做(圖二)块饺。這時(shí)架構(gòu)邏輯會(huì)清晰很多,但因?yàn)橹薪檎呷匀恍枰聪蛞蕾嚇I(yè)務(wù)模塊雌芽,這并沒(méi)有從根本上解除循壞依賴等問(wèn)題授艰。時(shí)不時(shí)發(fā)生一個(gè)模塊進(jìn)行改動(dòng),多個(gè)模塊受影響編譯不過(guò)的情況世落。進(jìn)一步的淮腾,通過(guò)技術(shù)手段,消除中介者對(duì)業(yè)務(wù)模塊依賴,即形成了業(yè)務(wù)模塊化架構(gòu)設(shè)計(jì)(圖三)谷朝。
<center>[圖片上傳失敗...(image-73c96-1564554721533)]</center>
通過(guò)業(yè)務(wù)模塊化架構(gòu)洲押,一般可以達(dá)到明確模塊職責(zé)及邊界,提升代碼質(zhì)量圆凰,減少?gòu)?fù)雜依賴杈帐,優(yōu)化編譯速度,提升開發(fā)效率等效果专钉。很多文章都有相關(guān)分析挑童,在此不再累述。

三跃须、業(yè)界常見模塊化方案

業(yè)務(wù)模塊化設(shè)計(jì)通過(guò)對(duì)各業(yè)務(wù)模塊的解耦改造站叼,避免循環(huán)雙向依賴,達(dá)到提升開發(fā)效率和質(zhì)量的目的回怜。但業(yè)務(wù)需求的依賴是無(wú)法消除的大年,所以模塊化方案首先要解決的是如何在無(wú)代碼依賴的情況下實(shí)現(xiàn)跨模塊通信的問(wèn)題。iOS 因?yàn)槠鋸?qiáng)大的運(yùn)行時(shí)特性玉雾,無(wú)論是基于 NSInvocation 還是基于 peformSelector 方法, 都可以很很容易做到這一點(diǎn)翔试。但不能為了解耦而解耦,提升質(zhì)量與效率才是我們的目的复旬。直接基于 hardcode 字符串 + 反射的代碼明顯會(huì)極大損害開發(fā)質(zhì)量與效率垦缅,與目標(biāo)背道而馳。所以驹碍,模塊化解耦需求的更準(zhǔn)確的描述應(yīng)該是“如何在保證開發(fā)質(zhì)量和效率的前提下做到無(wú)代碼依賴的跨模塊通信”壁涎。
目前業(yè)界常見的模塊間通訊方案大致如下幾種:

  1. 基于路由 URL 的 UI 頁(yè)面統(tǒng)跳管理。
  2. 基于反射的遠(yuǎn)程接口調(diào)用封裝志秃。
  3. 基于面向協(xié)議思想的服務(wù)注冊(cè)方案怔球。
  4. 基于通知的廣播方案。

根據(jù)具體業(yè)務(wù)和需求的不同浮还,大部分公司會(huì)采用以上一種或者某幾種的組合竟坛。

3.1 路由 URL 統(tǒng)跳方案

統(tǒng)跳路由是頁(yè)面解耦的最常見方式,大量應(yīng)用于前端頁(yè)面钧舌。通過(guò)把一個(gè) URL 與一個(gè)頁(yè)面綁定担汤,需要時(shí)通過(guò) URL 可以方便的打開相應(yīng)頁(yè)面。

//通過(guò)路由URL跳轉(zhuǎn)到商品列表頁(yè)面
//kRouteGoodsList = @"http://goods/goods_list"
UIViewController *vc = [Router handleURL:kRouteGoodsList]; 
if(vc) {
    [self.navigationController pushViewController:vc animated:YES];
}

當(dāng)然有些場(chǎng)景會(huì)比這個(gè)復(fù)雜洼冻,比如有些頁(yè)面需要更多參數(shù)崭歧。
基本類型的參數(shù),URL 協(xié)議天然支持:

//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ù)撞牢,可以提供一個(gè)額外的字典參數(shù) complexParams, 將復(fù)雜參數(shù)放到字典中即可:

+ (nullable id)handleURL:(nonnull NSString *)urlStr
           complexParams:(nullable NSDictionary*)complexParams
              completion:(nullable RouteCompletion)completion;

上面方法里的 completion 參數(shù)率碾,是一個(gè)回調(diào) block, 處理打開某個(gè)頁(yè)面需要有回調(diào)功能的場(chǎng)景叔营。比如打開會(huì)員選擇頁(yè)面,搜索會(huì)員播掷,搜到之后點(diǎn)擊確定审编,回傳會(huì)員數(shù)據(jù):

//kRouteMemberSearch = @“//member/member_search”
UIViewController *vc = [Router handleURL:urlStr complexParams:nil completion:^(id  _Nullable result) {
    //code to handle the result
    ...
}];
if(vc) {
    [self.navigationController pushViewController:vc animated:YES];
}

考慮到實(shí)現(xiàn)的靈活性,提供路由服務(wù)的頁(yè)面歧匈,會(huì)將 URL 與一個(gè) block 相綁定垒酬。block 中放入所需的初始化代碼〖可以在合適的地方將初始化 block 與路由 URL 綁定勘究,比如在 +load 方法里:

+ (void)load {
    [Router bindURL:kRouteGoodsList
           toHandler:^id _Nullable(NSDictionary * _Nullable parameters) {
        return [[GoodsListViewController alloc] init];
    }];
}

更多路由 URL 相關(guān)例子,可以參考 Bifrost 項(xiàng)目中的 Demo.

URL 本身是一種跨多端的通用協(xié)議斟冕。使用路由URL統(tǒng)跳方案的優(yōu)勢(shì)是動(dòng)態(tài)性及多端統(tǒng)一 (H5, iOS口糕,Android,Weex/RN); 缺點(diǎn)是能處理的交互場(chǎng)景偏簡(jiǎn)單磕蛇。所以一般更適用于簡(jiǎn)單 UI 頁(yè)面跳轉(zhuǎn)景描。一些復(fù)雜操作和數(shù)據(jù)傳輸,雖然也可以通過(guò)此方式實(shí)現(xiàn)秀撇,但都不是很效率超棺。
目前天貓和蘑菇街都有使用路由 URL 作為自己的頁(yè)面統(tǒng)跳方案,達(dá)到解耦的目的呵燕。

3.2 基于反射的遠(yuǎn)程調(diào)用封裝

當(dāng)無(wú)法 import 某個(gè)類的頭文件但仍需調(diào)用其方法時(shí)棠绘,最常想到的就是基于反射來(lái)實(shí)現(xiàn)了。例:

Class manager = NSClassFromString(@"YZGoodsManager");
NSArray *list = [manager performSelector:@selector(getGoodsList)];
//code to handle the list
...

但這種方式存在大量的 hardcode 字符串再扭。無(wú)法觸發(fā)代碼自動(dòng)補(bǔ)全氧苍,容易出現(xiàn)拼寫錯(cuò)誤,而且這類錯(cuò)誤只能在運(yùn)行時(shí)觸發(fā)相關(guān)方法后才能發(fā)現(xiàn)泛范。無(wú)論是開發(fā)效率還是開發(fā)質(zhì)量都有較大的影響让虐。

如何進(jìn)行優(yōu)化呢?這其實(shí)是各端遠(yuǎn)程調(diào)用都需要解決的問(wèn)題罢荡。移動(dòng)端最常見的遠(yuǎn)程調(diào)用就是向后端接口發(fā)網(wǎng)絡(luò)請(qǐng)求赡突。針對(duì)這類問(wèn)題,我們很容易想到創(chuàng)建一個(gè)網(wǎng)絡(luò)層柠傍,將這類“危險(xiǎn)代碼”封裝到里面麸俘。上層業(yè)務(wù)調(diào)用時(shí)網(wǎng)絡(luò)層接口時(shí)辩稽,不需要 hardcode 字符串惧笛,也不需要理解內(nèi)部麻煩的邏輯软免。

類似的妹萨,我可以將模塊間通訊也封裝到一個(gè)“網(wǎng)絡(luò)層”中(或者叫消息轉(zhuǎn)發(fā)層)且轨。這樣危險(xiǎn)代碼只存在某幾個(gè)文件里,可以特別地進(jìn)行 code review 和聯(lián)調(diào)測(cè)試螟够。后期還可以通過(guò)單元測(cè)試來(lái)保障質(zhì)量。模塊化方案中玫坛,我們可以稱這類“轉(zhuǎn)發(fā)層”為 Mediator (當(dāng)然你也可以起個(gè)別的名字)滋觉。同時(shí)因?yàn)?performSelector 方法附帶參數(shù)數(shù)量有限,也沒(méi)有返回值昌渤,所以更適合使用 NSInvocation 來(lái)實(shí)現(xiàn)赴穗。

//Mediator提供基于NSInvocation的遠(yuǎn)程接口調(diào)用方法的統(tǒng)一封裝
- (id)performTarget:(NSString *)targetName
             action:(NSString *)actionName
             params:(NSDictionary *)params;

//Goods模塊所有對(duì)外提供的方法封裝在一個(gè)Category中
@interface Mediator(Goods)
- (NSArray*)goods_getGoodsList;
- (NSInteger)goods_getGoodsCount;
...
@end
@impletation Mediator(Goods)
- (NSArray*)goods_getGoodsList {
    return [self performTarget:@“GoodsModule” action:@"getGoodsList" params:nil];
}
- (NSInteger)goods_getGoodsCount {
    return [self performTarget:@“GoodsModule” action:@"getGoodsCount" params:nil];
}
...
@end

然后各個(gè)業(yè)務(wù)模塊依賴Mediator, 就可以直接調(diào)用這些方法了。

//業(yè)務(wù)方依賴Mediator模塊膀息,可以直接調(diào)用相關(guān)方法
...
NSArray *list = [[Mediator sharedInstance] goods_getGoodsList];
...

這種方案的優(yōu)勢(shì)是調(diào)用簡(jiǎn)單方便般眉,代碼自動(dòng)補(bǔ)全和編譯時(shí)檢查都仍然有效。
劣勢(shì)是 category 存在重名覆蓋的風(fēng)險(xiǎn)潜支,需要通過(guò)開發(fā)規(guī)范以及一些檢查機(jī)制來(lái)規(guī)避甸赃。同時(shí) Mediator 只是收斂了 hardcode, 并未消除 hardcode, 仍然對(duì)開發(fā)效率有一定影響。

業(yè)界的 CTMediator 開源庫(kù)冗酿,以及美團(tuán)都是采用類似方案埠对。

3.3 服務(wù)注冊(cè)方案

有沒(méi)有辦法絕對(duì)的避免 hardcode 呢?如果接觸過(guò)后端的服務(wù)化改造裁替,會(huì)發(fā)現(xiàn)和移動(dòng)端的業(yè)務(wù)模塊化很相似项玛。Dubbo 就是服務(wù)化的經(jīng)典框架之一。它是通過(guò)服務(wù)注冊(cè)的方式來(lái)實(shí)現(xiàn)遠(yuǎn)程接口調(diào)用的胯究。即每個(gè)模塊提供自己對(duì)外服務(wù)的協(xié)議聲明稍计,然后將此聲明注冊(cè)到中間層。調(diào)用方能從中間層看到存在哪些服務(wù)接口裕循,然后直接調(diào)用即可臣嚣。例:

//Goods模塊提供的所有對(duì)外服務(wù)都放在GoodsModuleService中
@protocol GoodsModuleService
- (NSArray*)getGoodsList;
- (NSInteger)getGoodsCount;
...
@end
//Goods模塊提供實(shí)現(xiàn)GoodsModuleService的對(duì)象, 
//并在+load方法中注冊(cè)
@interface GoodsModule : NSObject<GoodsModuleService>
@end
@implementation GoodsModule
+ (void)load {
    //注冊(cè)服務(wù)
    [ServiceManager registerService:@protocol(service_protocol) 
                  withModule:self.class]
}
//提供具體實(shí)現(xiàn)
- (NSArray*)getGoodsList {...}
- (NSInteger)getGoodsCount {...}
@end

//將GoodsModuleService放在某個(gè)公共模塊中,對(duì)所有業(yè)務(wù)模塊可見
//業(yè)務(wù)模塊可以直接調(diào)用相關(guān)接口
...
id<GoodsModuleService> module = [ServiceManager objByService:@protocol(GoodsModuleService)];
NSArray *list = [module getGoodsList];
...

這種方式的優(yōu)勢(shì)也包括調(diào)用簡(jiǎn)單方便剥哑。代碼自動(dòng)補(bǔ)全和編譯時(shí)檢查都有效硅则。實(shí)現(xiàn)起來(lái)也簡(jiǎn)單,協(xié)議的所有實(shí)現(xiàn)仍然在模塊內(nèi)部株婴,所以不需要寫反射代碼了怎虫。同時(shí)對(duì)外暴露的只有協(xié)議,符合團(tuán)隊(duì)協(xié)作的“面向協(xié)議編程”的思想困介。劣勢(shì)是如果服務(wù)提供方和使用方依賴的是公共模塊中的同一份協(xié)議(protocol), 當(dāng)協(xié)議內(nèi)容改變時(shí)大审,會(huì)存在所有服務(wù)依賴模塊編譯失敗的風(fēng)險(xiǎn)。同時(shí)需要一個(gè)注冊(cè)過(guò)程座哩,將 Protocol 協(xié)議與具體實(shí)現(xiàn)綁定起來(lái)徒扶。

業(yè)界里,蘑菇街的 ServiceManager 和阿里的 BeeHive 都是采用的這個(gè)方案根穷。

3.4 通知廣播方案

基于通知的模塊間通訊方案姜骡,實(shí)現(xiàn)思路非常簡(jiǎn)單, 直接基于系統(tǒng)的 NSNotificationCenter 即可导坟。
優(yōu)勢(shì)是實(shí)現(xiàn)簡(jiǎn)單,非常適合處理一對(duì)多的通訊場(chǎng)景圈澈。
劣勢(shì)是僅適用于簡(jiǎn)單通訊場(chǎng)景惫周。復(fù)雜數(shù)據(jù)傳輸,同步調(diào)用等方式都不太方便康栈。
模塊化通訊方案中递递,更多的是把通知方案作為以上幾種方案的補(bǔ)充。

3.5 其它

除了模塊間通訊的實(shí)現(xiàn)啥么,業(yè)務(wù)模塊化架構(gòu)還需要考慮每個(gè)模塊內(nèi)部的設(shè)計(jì)漾狼,比如其生命周期控制,復(fù)雜對(duì)象傳輸饥臂,重復(fù)資源的處理等逊躁。可能因?yàn)槊總€(gè)公司都有自己的實(shí)際場(chǎng)景隅熙,業(yè)界方案里對(duì)這些問(wèn)題描述的并不是很多稽煤。但實(shí)際上他們非常重要,有贊在模塊化過(guò)程中做了很多相關(guān)思考和嘗試囚戚,會(huì)在后面環(huán)節(jié)進(jìn)行介紹酵熙。

四、有贊的模塊化實(shí)踐

有贊移動(dòng)自 16 年起開始實(shí)踐業(yè)務(wù)模塊化架構(gòu)方式驰坊,大致經(jīng)歷了 2016 年的嘗試+摸索匾二,2017 年的思考+優(yōu)化以及 2018 年的成熟+沉淀幾個(gè)階段。期間有過(guò)對(duì)已有 App 的模塊化改造拳芙,也試過(guò)直接應(yīng)用于新起項(xiàng)目察藐。模塊化方案經(jīng)歷過(guò)幾次改版,踩過(guò)一些坑舟扎,也收獲了很多寶貴的經(jīng)驗(yàn)分飞。

4.1 v1.0: 嘗試+摸索

16 年,有贊微信商城睹限、有贊收銀等 App 經(jīng)歷了初期的功能快速迭代譬猫,內(nèi)部依賴混亂,耦合嚴(yán)重羡疗,急需優(yōu)化重構(gòu)染服。傳統(tǒng)的 MVVM、MVP 等優(yōu)化方式無(wú)法從全局層面解決這些問(wèn)題叨恨。后來(lái)在 InfoQ 的"移動(dòng)開發(fā)前線"微信群里聽了蘑菇街的組件化方案分享柳刮,非常受啟發(fā)。不過(guò)當(dāng)時(shí)還是有一些顧慮,比如微信商城和收銀當(dāng)時(shí)都屬于中小型項(xiàng)目诚亚,每端開發(fā)人員都只有 4-6 人。業(yè)務(wù)模塊化改造后會(huì)形成一定的開發(fā)門檻午乓,帶來(lái)一定的開發(fā)效率下降站宗。小項(xiàng)目適合模塊化改造嗎?其收益是否能匹配付出呢益愈?但考慮到當(dāng)時(shí) App 各模塊邊界已經(jīng)穩(wěn)定梢灭,即使模塊化改造出現(xiàn)問(wèn)題,也可以用很小的代價(jià)將其降級(jí)到傳統(tǒng)的中介者模式蒸其,所以改造開始了敏释。

4.1.1 模塊間通信方式設(shè)計(jì)

首先是梳理我們的模塊間通信需求,主要包括以下三種:

  1. UI 頁(yè)面跳轉(zhuǎn)摸袁。比如IM模塊點(diǎn)擊用戶頭像打開會(huì)員模塊的用戶詳情頁(yè)钥顽。
  2. 動(dòng)作執(zhí)行及復(fù)雜數(shù)據(jù)傳輸。比如商品模塊向開單模塊傳遞商品數(shù)據(jù)模型并進(jìn)行價(jià)格計(jì)算靠汁。
  3. 一對(duì)多的通知廣播蜂大。比如 logout 時(shí)賬號(hào)模塊發(fā)出廣播,各業(yè)務(wù)模塊進(jìn)行 cache 清理及其它相應(yīng)操作蝶怔。

我們選擇了路由 URL + 遠(yuǎn)程接口調(diào)用封裝 + 廣播相結(jié)合的方式奶浦。

對(duì)于遠(yuǎn)程接口調(diào)用的封裝方式,我們沒(méi)有完全照抄 Mediator 方案踢星。當(dāng)時(shí)非常期望保留模塊化的編譯隔離屬性澳叉。比如當(dāng) A 模塊對(duì)外提供的某個(gè)接口發(fā)生變化時(shí),不會(huì)引發(fā)依賴這個(gè)接口的模塊的編譯錯(cuò)誤沐悦。這樣可以避免依賴模塊被迫中斷手頭的工作先去解決編譯問(wèn)題成洗。當(dāng)時(shí)也沒(méi)有采用Beehive的服務(wù)注冊(cè)方式,也是因?yàn)橥瑯拥脑颉?經(jīng)過(guò)討論藏否,當(dāng)時(shí)選擇參考網(wǎng)絡(luò)層封裝方式泌枪,在每個(gè)模塊中設(shè)計(jì)一個(gè)對(duì)外的“網(wǎng)絡(luò)層” ModuleService。將對(duì)其它模塊的接口的反射調(diào)用秕岛,放入各個(gè)模塊的 ModuleService 中碌燕。
同時(shí),我們希望各業(yè)務(wù)模塊不需要去理解所依賴模塊的內(nèi)部復(fù)雜實(shí)現(xiàn)继薛。比如 A 模塊依賴 D 模塊的 class D1 的接口 method1修壕, class D2 的接口method2, class D3 的接口 method3. A 需要了解 D 模塊的這些內(nèi)部信息才能完成反射功能的實(shí)現(xiàn)。如果 D 模塊中這些命名有所變化遏考,還會(huì)出現(xiàn)調(diào)用失敗慈鸠。所以我們對(duì)各個(gè)模塊使用外觀(Facade)模式進(jìn)行重構(gòu)。D 模塊創(chuàng)建一個(gè)外觀層 FacadeD. 通過(guò) FacadeD 對(duì)象對(duì)外提供所有服務(wù)灌具,同時(shí)隱藏內(nèi)部復(fù)雜實(shí)現(xiàn)青团。調(diào)用方也只需要理解 FacadeD 的頭文件 包含哪些接口即可譬巫。

外觀(Facade)模式: 為子系統(tǒng)中的一組接口提供一個(gè)一致的界面, Facade 模式定義了一個(gè)高層接口督笆,這個(gè)接口使得這一子系統(tǒng)更加容易使用芦昔。引入外觀角色之后,用戶只需要直接與外觀角色交互娃肿,用戶與子系統(tǒng)之間的復(fù)雜關(guān)系由外觀角色來(lái)實(shí)現(xiàn)咕缎,從而降低了系統(tǒng)的耦合度。
<center>[圖片上傳失敗...(image-32690c-1564554721534)]</center>

另外料扰,為什么還需要路由 URL 呢凭豪?
其實(shí)從功能角度,遠(yuǎn)程接口的網(wǎng)絡(luò)層晒杈,完全可以取代路由 URL 實(shí)現(xiàn)頁(yè)面跳轉(zhuǎn)嫂伞,而且沒(méi)有路由 URL 的一些 hardcode 的問(wèn)題。而且路由 URL 和
遠(yuǎn)程接口存在一定的功能重合拯钻,還會(huì)造成后續(xù)實(shí)現(xiàn)新功能時(shí)末早,分不清應(yīng)選擇路由 URL 還是選擇遠(yuǎn)程接口的困惑。這里選擇支持路由 URL 的主要原因是我們存在動(dòng)態(tài)化且多端統(tǒng)一的需求说庭。比如消息模塊下發(fā)的各種消息數(shù)據(jù)模型完全是動(dòng)態(tài)的然磷。后端配好展示內(nèi)容以及跳轉(zhuǎn)需求后,客戶端不需要理解具體需求刊驴,只需要通過(guò)統(tǒng)一的路由跳轉(zhuǎn)協(xié)議執(zhí)行跳轉(zhuǎn)動(dòng)作即可姿搜。

4.1.2 模塊內(nèi)設(shè)計(jì)及 App 結(jié)構(gòu)調(diào)整

每個(gè)模塊除了 Facade 模式改造之外,還需要考慮以下問(wèn)題:

  1. 合適的注冊(cè)及初始化方式捆憎。
  2. 接收并處理全局事件舅柜。
  3. App 層和 Common 層設(shè)計(jì)。
  4. 模塊編譯產(chǎn)出以及集成到 App 中的方式躲惰。

因?yàn)榭紤]到每個(gè) App 中業(yè)務(wù)模塊數(shù)量不會(huì)很多(我們幾個(gè) App 內(nèi)大多是20個(gè)左右)致份,所以我們?yōu)槊總€(gè)模塊創(chuàng)建了一個(gè) Module 對(duì)象并令其為單例。在 +load 方法中將自身注冊(cè)給模塊化 SDK Bifrost. 經(jīng)測(cè)試础拨,這里因?yàn)閱卫斐傻膬?nèi)存占用以及 +load 方法引起的啟動(dòng)速度影響都微乎其微氮块。模塊需要監(jiān)聽的全局事件主要為 UIApplicationDelegate 中的那些方法。所以我們定義了一個(gè)繼承 UIApplicationDelegate 的協(xié)議 BifrostModuleProtocol诡宗,令每個(gè)模塊的 Module 對(duì)象都服從這個(gè)協(xié)議滔蝉。App 的 AppDelegate對(duì)象,會(huì)輪詢所有注冊(cè)了的業(yè)務(wù)模塊并進(jìn)行必要的調(diào)用塔沃。

@protocol BifrostModuleProtocol <UIApplicationDelegate, NSObject>
@required
+ (instancetype)sharedInstance;
- (void)setup;
...
@optional
+ (BOOL)setupModuleSynchronously;
...
@end

所有業(yè)務(wù)代碼挪入各業(yè)務(wù)模塊的 Module 對(duì)象后蝠引,AppDelegate 非常干凈。

@implementation YZAppDelegate
- (BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [Bifrost setupAllModules];
    [Bifrost checkAllModulesWithSelector:_cmd arguments:@[Safe(application), Safe(launchOptions)]];
    return YES;
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [Bifrost checkAllModulesWithSelector:_cmd arguments:@[Safe(application), Safe(launchOptions)]];
    return YES;
}
- (void)applicationWillEnterForeground:(UIApplication *)application {
    [Bifrost checkAllModulesWithSelector:_cmd arguments:@[Safe(application)]];
}
...
@end

每個(gè)業(yè)務(wù)模塊都作為一個(gè)子 Project 集成入 App Project. 同時(shí)創(chuàng)建一個(gè)特殊的模塊 Common,用于放置一些通用業(yè)務(wù)和全局的基類螃概。App 層只保留 AppDelegate 等全局類和 plist 等特殊配置矫夯,基本沒(méi)有任何業(yè)務(wù)代碼。Common 層因?yàn)闆](méi)有明確的業(yè)務(wù)組來(lái)負(fù)責(zé)吊洼,所以也應(yīng)該盡量輕薄训貌。各業(yè)務(wù)模塊之間互不可見,但可以直接依賴 Common 模塊融蹂。通過(guò)search path來(lái)設(shè)置模塊依賴關(guān)系。

每個(gè)業(yè)務(wù)模塊的產(chǎn)出包括可執(zhí)行文件和資源文件兩部分弄企。有2種選擇:生成 framework 和生成靜態(tài)庫(kù) + 資源 bundle.

使用framework的優(yōu)點(diǎn)是輸出在同一個(gè)對(duì)象內(nèi)超燃,方便管理。缺點(diǎn)是作為動(dòng)態(tài)庫(kù)載入拘领,影響加載速度意乓。所以當(dāng)時(shí)選擇了靜態(tài)庫(kù) + bundle 的形式。不過(guò)個(gè)人感覺(jué)這塊還是需要具體測(cè)一下會(huì)慢做少再做決定更合適约素。但因?yàn)槎卟顒e不大届良,所以后續(xù)我們也一直沒(méi)作調(diào)整。

另外如果使用framework圣猎,需要注意資源讀取的問(wèn)題士葫。因?yàn)閭鹘y(tǒng)的資源讀取方式無(wú)法定位到framework內(nèi)資源,需要通過(guò) bundleForClass: 才行送悔。

//傳統(tǒng)方式只能定位到指定bundle慢显,比如main bundle中資源
NSURL *path = [[NSBundle mainBundle] URLForResource:@"file_name" withExtension:@"txt"]; 

// framework bundle需要通過(guò)bundleForClass獲取
NSBundle *bundle = [NSBundle bundleForClass:classA]; //classA為framework中的某各類
// 讀UIStoryboard
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@“sb_name” bundle:bundle];
// 讀UIImage
UIImage *image = [UIImage imageNamed:@"icon_name" inBundle:bundle compatibleWithTraitCollection:nil];
...

4.1.3 復(fù)雜對(duì)象傳輸

當(dāng)時(shí)最糾結(jié)的點(diǎn)就是復(fù)雜對(duì)象的傳輸。例如商品模型欠啤,它包含幾十個(gè)字段荚藻。如果是傳字典或傳 json, 那么數(shù)據(jù)提供方(商品模塊)和使用方(開單模塊)都需要專門理解并實(shí)現(xiàn)一下這種模型的各種字段,對(duì)開發(fā)效率影響很大.

有沒(méi)有辦法直接傳遞模型對(duì)象呢洁段?這里涉及到模型的類文件放在哪里应狱。最容易想到的方案是沉入 Common 模塊。但一旦這個(gè)口子放開祠丝,后續(xù)會(huì)有越來(lái)越多的模型放入 Common疾呻,和前面提到的簡(jiǎn)化 Common 層的目標(biāo)是相悖的。而且因?yàn)?Common 模塊沒(méi)有明確業(yè)務(wù)組歸屬写半,所有小組都能編輯, 其質(zhì)量和穩(wěn)定性難以保障罐韩。最終我們采用了一個(gè) tricky 的方案,把要傳遞的復(fù)雜模型的代碼復(fù)制一份放在使用方模塊中污朽,同時(shí)通過(guò)修改類名前綴加以區(qū)分散吵,這樣就可以避免打包時(shí)的鏈接沖突錯(cuò)誤。比如商品模塊內(nèi)叫 YZGGoodsModel, 開單模塊內(nèi)叫 YZSGoodsModel. 商品模塊的接口返回的是 YZGGoodsModel,開單模塊將其強(qiáng)轉(zhuǎn)為 YZSGoodsModel 即可矾睦。

//YZSaleModuleService.m內(nèi)
#import "YZSGoodsModel.h"

- (YZSGoodsModel*)goodsById:(NSString*)goodsId {
    //Sale Module遠(yuǎn)程調(diào)用Goods Module的接口
    id obj = [Bifrost performTarget:@"YZGoodsModule"
                           action:@"goodsById:"
                             params:@[goodsId]];
    //做一次強(qiáng)轉(zhuǎn)
    YZSGoodsModel *goods = (YZSGoodsModel*)obj;
    return goods;
}

這種方式雖然比較粗暴晦款,但考慮到兩個(gè)模塊間交互的復(fù)雜對(duì)象應(yīng)該不會(huì)很多(如果太多則應(yīng)考慮這兩個(gè)模塊是否劃分合適),同時(shí)拷貝粘貼操作起來(lái)成本可控枚冗,所以可以接受缓溅。同時(shí)這種方法也能達(dá)到預(yù)期的編譯隔離的效果。但兩邊模型定義及實(shí)現(xiàn)還是有不一致的風(fēng)險(xiǎn)赁温。為了解決一致性問(wèn)題坛怪,我們做了個(gè)檢查腳本工具,在編譯時(shí)觸發(fā)股囊。會(huì)根據(jù)命名規(guī)則查找這類“同名” model 的代碼袜匿,并做一個(gè)比較。如果發(fā)現(xiàn)不一致稚疹,則報(bào) warning. 注意不是報(bào)error, 因?yàn)槲覀兿M粋€(gè)模塊做了接口修改居灯,另一個(gè)模塊可以存在一種選擇,是馬上更新接口内狗,還是先完成手頭的工作將來(lái)再更新怪嫌。

4.1.4 重復(fù)資源處理

這類資源主要包括圖片、音視頻柳沙,數(shù)據(jù)模型等等岩灭。

首先我們排除了無(wú)腦放入 Common 的方案。因?yàn)橄鲁寥?Common 會(huì)破壞各業(yè)務(wù)模塊的完整性赂鲤,同時(shí)也會(huì)影響 Common 的質(zhì)量川背。經(jīng)過(guò)討論后,決定把資源分為三類:

  1. 通用功能所用資源蛤袒,將相關(guān)代碼整理為功能組件后一起放入 Common.
  2. 業(yè)務(wù)功能的大部分資源可以通過(guò)無(wú)損壓縮控制體積熄云,體積不大的資源允許一定程度上的重復(fù)。
  3. 較大體積的資源放到服務(wù)端妙真,App 端動(dòng)態(tài)拉取放在本地緩存中缴允。

同時(shí)平時(shí)定期通過(guò)自動(dòng)化工具檢測(cè)無(wú)用資源,以及重復(fù)資源的大小珍德,以便及時(shí)優(yōu)化包體積练般。

4.1.5 體驗(yàn)與成果

基于以上設(shè)計(jì),我們大概花了 3 的個(gè)月的時(shí)間對(duì)已有項(xiàng)目進(jìn)行了業(yè)務(wù)模塊化改造(邊做業(yè)務(wù)邊改造)锈候。因?yàn)榉桨讣?xì)節(jié)考慮的比較多薄料,大家對(duì)一些可能存在的問(wèn)題也都有預(yù)期,所以當(dāng)時(shí)改造后大家多持肯定態(tài)度泵琳,成本 vs 收益還是可觀的摄职。

v1.0 版本改造后誊役,App 架構(gòu)關(guān)系如圖:
<center>[圖片上傳失敗...(image-817f0f-1564554721534)]</center>
App 項(xiàng)目結(jié)構(gòu)如圖:

<img src="https://tech.youzan.com/content/images/2019/05/project1-1.png" width = 180 />

4.2 v2.0: 思考+優(yōu)化

16 年的第一版模塊化設(shè)計(jì)方案雖然可行,但還存在兩個(gè)痛點(diǎn):

  1. 模塊間網(wǎng)絡(luò)層的封裝基于反射代碼,寫起來(lái)仍然有些麻煩谷市。而且需要額外寫單測(cè)保證質(zhì)量蛔垢。
  2. 復(fù)雜對(duì)象的處理方式也存在一些問(wèn)題,比如拷貝粘貼的方式比較丑陋迫悠,重復(fù)代碼會(huì)帶來(lái)包體積的增加鹏漆。

上述問(wèn)題在團(tuán)隊(duì)規(guī)模擴(kuò)大,新同學(xué)到來(lái)時(shí)格外明顯创泄,經(jīng)常需要答疑講解艺玲。甚至有一次業(yè)務(wù)項(xiàng)目時(shí)間特別緊張時(shí),有些小伙伴私下更改模塊間頭文件 search path鞠抑,直接依賴的了別的模塊饭聚,以便重用復(fù)雜模型類的情況。

這些問(wèn)題的根本原因還是存在效率損失碍拆,"不方便"若治,怎么優(yōu)化呢慨蓝?

4.2.1 遠(yuǎn)程接口封裝優(yōu)化

首先是如何避免反射及 hardcode. 阿里 Beehive 的基于服務(wù)注冊(cè)的方式 是不需要 hardcode 代碼的感混。但它有額外的服務(wù)注冊(cè)過(guò)程,可能會(huì)影響啟動(dòng)速度礼烈,性能弱于基于反射的接口封裝方案弧满。這里對(duì)啟動(dòng)速度的影響究竟有多少呢?我們做了個(gè)測(cè)試此熬,在 +load 方法中注冊(cè)了 1000 個(gè) Sevice Protocol, 啟動(dòng)時(shí)間影響大概是 2-4 ms, 非常少庭呜。
<center>[圖片上傳失敗...(image-4c0f4b-1564554721534)]</center>
因?yàn)槲覀兠總€(gè)模塊都是基于外觀模式設(shè)計(jì)的。所以每個(gè)模塊只需要對(duì)外暴露一個(gè) Service Protocol 即可犀忱。我們 App 的實(shí)際模塊數(shù)量大概是 20 個(gè)募谎,所以對(duì)啟動(dòng)速度的影響可以忽略不計(jì)。而且前文提到阴汇,每個(gè)模塊本來(lái)也需要注冊(cè)自己的外觀類(Module 對(duì)象)以處理生命周期和接受 AppDelegate 消息数冬。這里 Service Protocl 的實(shí)現(xiàn)者就是這個(gè) Module 對(duì)象,所以其實(shí)沒(méi)有額外的性能消耗搀庶。

4.2.2 復(fù)雜對(duì)象傳輸優(yōu)化

之前的業(yè)務(wù)模塊化方案沒(méi)有使用 Beehive 還有個(gè)原因拐纱,就是服務(wù)提供方和使用方共同依賴同一個(gè) Protocol,不符合我們編譯隔離的需求哥倔。但既然我們可以拷貝粘貼復(fù)雜對(duì)象代碼秸架,是否也可以拷貝粘貼 Protocol 聲明呢?答案是可行的咆蒿。而且即使工程中同時(shí)存在多個(gè)同名的 Protocol 也不會(huì)引起編譯問(wèn)題东抹,連改名這一步都省去了蚂子。以商品模型為例,為它定義一個(gè) GoodModelProtocol, 服務(wù)使用方開單模塊可以直接將這個(gè) Protocol 的聲明 copy 到自己模塊中府阀,也不需要改名缆镣,操作成本非常低。然后商品模塊內(nèi)就可以使用這個(gè) Protocol 了试浙。同時(shí)因?yàn)橛玫氖峭粋€(gè)協(xié)議對(duì)象董瞻,所以 v1.0 中的類型強(qiáng)轉(zhuǎn)風(fēng)險(xiǎn)也沒(méi)有了。

跨模塊進(jìn)行方法調(diào)用和數(shù)據(jù)讀取非常便捷:

NSString *goodsID = @"123123123";
id<YZGoodsModelProtocol> goods = [BFModule(YZGoodsModuleService) goodsById:goodsID];
self.goodsCell.name = goods.name;
self.goodsCell.price = goods.price;
...

為盡量減少拷貝粘貼頻率田巴,我們將每個(gè)模塊對(duì)外提供的接口服務(wù)钠糊,路由定義,通知定義壹哺,以及復(fù)雜對(duì)象 Protocol 定義都放在 ModuleService.h 中抄伍。管理非常方便規(guī)范,別的模塊 copy 起來(lái)也簡(jiǎn)單管宵,只需要把這個(gè) ModuleService.h 文件
copy 到自己模塊內(nèi)部截珍,就可以直接依賴并調(diào)用接口了。而且如果將來(lái)需要從服務(wù)器拉取相關(guān)配置箩朴,一個(gè)文件會(huì)方便很多岗喉。但是也需要考慮如果以上內(nèi)容都放入同一個(gè)頭文件,會(huì)不會(huì)導(dǎo)致文件過(guò)大的問(wèn)題炸庞。當(dāng)時(shí)分析模塊間交互是有限的钱床,否則就需要考慮模塊劃分是否合適。所以問(wèn)題應(yīng)該不大埠居。從結(jié)果來(lái)看查牌,目前我們最大的 ModuleService.h, 加上注釋大概是 300 多行。

4.2.3 其它優(yōu)化

另外滥壕,我們發(fā)現(xiàn)每個(gè)模塊對(duì)初始化順序也有需求纸颜。比如賬號(hào)模塊的初始化可能要優(yōu)先于別的模塊,以便別的模塊在初始化時(shí)使用其服務(wù)绎橘。所以我們也對(duì) ModuleProtocol 增加了優(yōu)先級(jí)接口胁孙。每個(gè)模塊可以定義自己的初始化優(yōu)先級(jí)。

/**
 The priority of the module to be setup. 0 is the lowest priority;
 If not provided, the default priority is BifrostModuleDefaultPriority;

 @return the priority
 */
+ (NSUInteger)priority;

經(jīng)過(guò)以上優(yōu)化改造金踪,基本解決了 v1.0 的所有質(zhì)量及效率方面的隱患浊洞,業(yè)務(wù)模塊化方案趨近成熟。

4.3 v3.0: 成熟+沉淀

17 年優(yōu)化后的模塊化方案胡岔,基本算是具有有贊特色的相對(duì)成熟的方案了法希,支撐了包括零售在內(nèi)的多個(gè)大型app的開發(fā)。

4.3.1 編譯隔離的思考

Copy 頭文件的方式仍然有一些理解成本靶瘸。移動(dòng)團(tuán)隊(duì)規(guī)纳灰啵快速發(fā)展毛肋,一些新來(lái)的小伙伴還是會(huì)提出疑問(wèn)。18 年年中我們做了幾次檢查屋剑,發(fā)現(xiàn)模塊間 ModuleService 版本不一致的情況時(shí)有發(fā)生润匙。當(dāng)時(shí)零售移動(dòng)團(tuán)隊(duì)雖然達(dá)到 30 多人,但仍然是一個(gè)協(xié)作緊密的整體唉匾,發(fā)版節(jié)奏基本一致孕讳。各業(yè)務(wù)模塊代碼都在同一個(gè) git 工程中,基本每次發(fā)版用的都是各個(gè)模塊的最新版本巍膘。而且實(shí)際做了幾次調(diào)查厂财,發(fā)現(xiàn) ModuleService 中接口改變導(dǎo)致的依賴模塊的修改,其實(shí)成本很低峡懈,改起來(lái)很快璃饱。此時(shí)我們開始思考之前追求的編譯隔離是否適合當(dāng)前階段,是否有實(shí)際價(jià)值肪康。

最終我們決定節(jié)省每一份精力荚恶,效率最大化。將各業(yè)務(wù)的 ModuleService進(jìn)行下沉到 Commom 模塊磷支,各業(yè)務(wù)模塊直接依賴 Common 中的這些 ModuleServie 頭文件谒撼,不再需要 copy 操作。這樣改造的代價(jià)是形成了更多的依賴齐唆。本來(lái)一個(gè)業(yè)務(wù)模塊是可以不依賴 Common 的嗤栓,但現(xiàn)在就必須依賴了冻河。但考慮到實(shí)際情況箍邮,還沒(méi)有不依賴 Common 的業(yè)務(wù)模塊存在,這種追求沒(méi)有價(jià)值叨叙,所以應(yīng)該問(wèn)題不大锭弊。同時(shí)因?yàn)橄鲁恋亩际且恍╊^文件,沒(méi)有具體實(shí)現(xiàn)擂错,將來(lái)如果需要模塊間的進(jìn)一步隔離味滞,比如模塊單獨(dú)打包等,只需要將這些 Moduleservie 做到服務(wù)端可配置 + 自動(dòng)化下載生成即可钮呀,改造成本非常小剑鞍。

但這樣改造后又發(fā)生了一件事。某個(gè)新來(lái)的同學(xué)爽醋,直接在 Common 模塊中寫代碼通過(guò)這些 ModuleService 調(diào)用了上層業(yè)務(wù)模塊的功能蚁署,形成了底層 Commmon 模塊對(duì)上層業(yè)務(wù)模塊的反向依賴。于是我們進(jìn)一步拆分出了一個(gè)新模塊 Mediator, 將 Bifrost SDK 和這些 ModuleSevice 放入其中蚂四。Common 模塊和 Mediator 互不可見光戈。

最終形成的 App 架構(gòu)為:
<center>[圖片上傳失敗...(image-a8fb5a-1564554721534)]</center>

:業(yè)界有些方案是把 ModuleServie 分開存放的哪痰,相當(dāng)于把以上方案里的 Mediator 部分進(jìn)行分拆,每個(gè)業(yè)務(wù)模塊都有一個(gè)久妆。這種方式的優(yōu)點(diǎn)是職責(zé)明確晌杰,大家不用同時(shí)對(duì)一個(gè)公共模塊進(jìn)行修改,同時(shí)可以做到依賴關(guān)系很清晰筷弦;劣勢(shì)是模塊的數(shù)量增加了一倍肋演,維護(hù)成本增加很多±们伲考慮到我們目前的情況惋啃,Mediator 模塊是很薄的一層,共同修改維護(hù)這個(gè)模塊也可以接受监右,所以目前沒(méi)有將其拆開边灭。將來(lái)如果需要,再將其做分拆改造即可健盒,改造工作量很小绒瘦。

4.3.2 代碼隔離的思考

除了不在不合適的階段追求編譯隔離,我們還發(fā)現(xiàn)代碼隔離并不適合我們扣癣。

業(yè)務(wù)模塊化的效果之一就是個(gè)業(yè)務(wù)模塊可以單獨(dú)打包惰帽,放入殼工程運(yùn)行。很容易想到的一個(gè)改造就是把各個(gè)模塊拆到不同的 git 中父虑。好處很多该酗,比如單獨(dú)的權(quán)限控制,獨(dú)立的版本號(hào)士嚎,萬(wàn)一發(fā)版時(shí)發(fā)現(xiàn)問(wèn)題可以及時(shí) rollback 用老版本打包呜魄。我們的微信商城 App 就做了這種嘗試。將代碼遷到了很多 git 中莱衩,通過(guò) pod 的方式進(jìn)行管理爵嗅。但后續(xù)開發(fā)中體驗(yàn)并不是很好。當(dāng)時(shí)微信商城 App 的模塊數(shù)量比開發(fā)同學(xué)數(shù)量多很多笨蚁,每個(gè)同學(xué)都同時(shí)維護(hù)著多個(gè)模塊睹晒。有時(shí)一個(gè)項(xiàng)目,一個(gè)人需要同時(shí)在多個(gè) git 中修改多個(gè)模塊的代碼括细。修改完成后伪很,要多次執(zhí)行提交、打版本號(hào)以及集成測(cè)試等操作奋单,很不效率锉试。同時(shí)因?yàn)樯婕暗蕉鄠€(gè) git,代碼提交的 Merge Request 和相關(guān)的編譯檢查也復(fù)雜了很多辱匿。同樣的键痛,因?yàn)槲⑿派坛?App 中不同模塊的開發(fā)發(fā)版節(jié)奏也基本一致炫彩,所以多 git 多 pod 的不同版本管理及回退的優(yōu)勢(shì)也沒(méi)有體現(xiàn)出來(lái)。最終還是將各模塊代碼遷回了主 git 中絮短。

4.3.3 沒(méi)價(jià)值的隔離江兢?

但編譯隔離和代碼隔離真的沒(méi)有價(jià)值嗎?當(dāng)然不是丁频,主要是我們當(dāng)前階段并不需要杉允。過(guò)早的調(diào)整增加了成本卻沒(méi)有價(jià)值產(chǎn)出,所以并不合適席里。實(shí)際上我們還有一些業(yè)務(wù)模塊是跨 App 使用的叔磷,比如IM模塊,資產(chǎn)模塊等等奖磁。他們都是獨(dú)立 git 獨(dú)立發(fā)版的改基。編譯隔離和代碼隔離屬性對(duì)他們很有效。

另外咖为,每個(gè)模塊單獨(dú)git可以有更細(xì)粒度的權(quán)限管理秕狰。我們因?yàn)樵谝粋€(gè)git中,曾發(fā)生過(guò)好幾次小伙伴改別人的模塊改出問(wèn)題的例子(雖然有MR, 但人難免有遺漏)躁染。后來(lái)我們是通過(guò) git commit hook + 修改文件路徑來(lái)控制修改權(quán)限才解決了這個(gè)問(wèn)題鸣哀。后續(xù)介紹有贊移動(dòng)基礎(chǔ)設(shè)施建設(shè)的文章中會(huì)有更多相關(guān)細(xì)節(jié)。

4.3.4 Bifrost (雷神里的彩虹橋)

最終吞彤,我們總結(jié)了所有我們需要的業(yè)務(wù)模塊化需求我衬,沉淀出了輕量級(jí)的模塊化 SDK Bifrost.

為什么不直接使用業(yè)界的 CTMediator 或者 Beehive 或者 MGJRouter, 要再造個(gè)輪子呢?主要有三個(gè)原因:一是我們開始嘗試模塊化改造時(shí)饰恕,業(yè)界還沒(méi)有相關(guān)框架開源出來(lái)挠羔,所以需要自己實(shí)現(xiàn)。二是我們的需求和業(yè)界的開源庫(kù)不完全相符懂盐。MGJRouter 缺少服務(wù)管理褥赊,CTMediator 和設(shè)計(jì)不符糕档,Beehive 沒(méi)有路由管理同時(shí)不夠輕量(很多接口還是基于阿里的需求提供的莉恼,我們用不到,會(huì)形成理解成本)速那。原因三其實(shí)是最關(guān)鍵的俐银,就是模塊化 SDK 的實(shí)現(xiàn)其實(shí)不難。通過(guò)前面的介紹端仰,可以發(fā)現(xiàn)其中并沒(méi)有什么黑魔法捶惜,代碼量也不多,實(shí)現(xiàn)成本很低荔烧。模塊化過(guò)程更多精力花在了全局架構(gòu)設(shè)計(jì)吱七,與之配合的開發(fā)規(guī)范汽久,以及結(jié)合自己團(tuán)隊(duì)情況的一些取舍。模塊化 SDK 只是模塊化整體設(shè)計(jì)的冰山一角踊餐。我們也推薦讀者所在團(tuán)隊(duì)景醇,如果有時(shí)間可以嘗試自己實(shí)現(xiàn)模塊化工具,Bifrost 只用做參考即可吝岭。

4.3.5 業(yè)務(wù)模塊化時(shí)機(jī)

我們建議所有進(jìn)入業(yè)務(wù)領(lǐng)域劃分穩(wěn)定期(業(yè)務(wù)模塊基本確定三痰,不會(huì)發(fā)生較大變動(dòng))的團(tuán)隊(duì)采用業(yè)務(wù)模塊化架構(gòu)設(shè)計(jì)。即使模塊劃分還沒(méi)完全明確窜管,也可以考慮對(duì)部分明確了模塊進(jìn)行模塊化改造散劫。因?yàn)檫t早要用,晚用不如早用幕帆。目前基于路由 URL + 協(xié)議注冊(cè)的模塊間通訊方式获搏,對(duì)開發(fā)效率基本無(wú)損。

五失乾、總結(jié)

移動(dòng)應(yīng)用的業(yè)務(wù)模塊化架構(gòu)設(shè)計(jì)颜凯,其真正的目標(biāo)是提升開發(fā)質(zhì)量和效率。單從實(shí)現(xiàn)角度來(lái)看并沒(méi)有什么黑魔法或技術(shù)難點(diǎn)仗扬,更多的是結(jié)合團(tuán)隊(duì)實(shí)際開發(fā)協(xié)作方式和業(yè)務(wù)場(chǎng)景的具體考量——“適合自己的才是最好的”症概。有贊移動(dòng)團(tuán)隊(duì)通過(guò)過(guò)往3年的實(shí)踐,發(fā)現(xiàn)一味的追求性能早芭,絕對(duì)的追求模塊間編譯隔離彼城,過(guò)早的追求模塊代碼管理隔離等方式都偏離了模塊化設(shè)計(jì)的真正目的,是得不償失的退个。更合適的方式是在可控的改造代價(jià)下募壕,一定程度考慮未來(lái)的優(yōu)化方式,更多的考慮當(dāng)前的實(shí)際場(chǎng)景语盈,來(lái)設(shè)計(jì)適合自己的模塊化方式舱馅。希望通過(guò)本文提供的具體案例和思考方式,大家都能找到適合自己應(yīng)用的業(yè)務(wù)模塊化之路刀荒。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末代嗤,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子缠借,更是在濱河造成了極大的恐慌干毅,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泼返,死亡現(xiàn)場(chǎng)離奇詭異硝逢,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門渠鸽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)叫乌,“玉大人,你說(shuō)我怎么就攤上這事徽缚∽劢妫” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵猎拨,是天一觀的道長(zhǎng)膀藐。 經(jīng)常有香客問(wèn)我,道長(zhǎng)红省,這世上最難降的妖魔是什么额各? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮吧恃,結(jié)果婚禮上虾啦,老公的妹妹穿的比我還像新娘。我一直安慰自己痕寓,他們只是感情好傲醉,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著呻率,像睡著了一般硬毕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上礼仗,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天吐咳,我揣著相機(jī)與錄音,去河邊找鬼元践。 笑死韭脊,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的单旁。 我是一名探鬼主播沪羔,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼象浑!你這毒婦竟也來(lái)了蔫饰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤融柬,失蹤者是張志新(化名)和其女友劉穎死嗦,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體粒氧,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年节腐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了外盯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片摘盆。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖饱苟,靈堂內(nèi)的尸體忽然破棺而出孩擂,到底是詐尸還是另有隱情,我是刑警寧澤箱熬,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布类垦,位于F島的核電站,受9級(jí)特大地震影響城须,放射性物質(zhì)發(fā)生泄漏蚤认。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一糕伐、第九天 我趴在偏房一處隱蔽的房頂上張望砰琢。 院中可真熱鬧,春花似錦良瞧、人聲如沸陪汽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)挚冤。三九已至,卻和暖如春赞庶,著一層夾襖步出監(jiān)牢的瞬間你辣,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工尘执, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留舍哄,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓誊锭,卻偏偏與公主長(zhǎng)得像表悬,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子丧靡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354