客戶端模塊化解耦實(shí)踐 - Router
背景
隨著業(yè)務(wù)進(jìn)入快速發(fā)展期烫葬,業(yè)務(wù)線拓展迅速荷辕,項(xiàng)目結(jié)構(gòu)變得龐大復(fù)雜缩膝,導(dǎo)致迭代的成本越來(lái)越高。項(xiàng)目的越發(fā)龐大也使得整個(gè)工程編譯時(shí)間越來(lái)越長(zhǎng)。進(jìn)行項(xiàng)目拆分后模塊化運(yùn)行(項(xiàng)目自運(yùn)行躁锁、自管理)是一個(gè)較好的出路,但項(xiàng)目間的復(fù)雜相互引用導(dǎo)致我們無(wú)從下手卵史,而如何去除這些項(xiàng)目間的引用則是今天的主題
產(chǎn)生
首先我們可以整理下現(xiàn)狀战转,我們的碰到的最大問(wèn)題其實(shí)就是項(xiàng)目間雜亂無(wú)章的耦合、引用
針對(duì)這些耦合以躯、依賴槐秧,我們可以簡(jiǎn)單的分析:從理論上來(lái)說(shuō),每個(gè)項(xiàng)目負(fù)責(zé)單獨(dú)的業(yè)務(wù)忧设,應(yīng)該本身就是相互獨(dú)立的存在刁标。但是實(shí)際上,處于同一個(gè)公司業(yè)務(wù)體系中址晕,項(xiàng)目與項(xiàng)目之間不可能真正做到完全的切割膀懈。(公司與公司間都可能存在合作,項(xiàng)目與項(xiàng)目間當(dāng)然就更可能了)
看來(lái)想從業(yè)務(wù)源頭想去除業(yè)務(wù)依賴是不太可能了谨垃,我們只能從我們技術(shù)角度去進(jìn)行項(xiàng)目間耦合启搂、引用的去除了。而其中最關(guān)鍵的一點(diǎn)就是:耦合刘陶、相互依賴 , 而Router的概念正是為了解決這些問(wèn)題而提出的胳赌。
那Router到底是什么呢?
Router是一個(gè)具有收口匙隔、分發(fā)思想的疑苫、用于處理模塊(項(xiàng)目)間直接依賴、調(diào)用的模型纷责。最直接的體現(xiàn)其實(shí)就是一個(gè)約定捍掺、一個(gè)規(guī)則,按照約定碰逸、規(guī)則進(jìn)行解析乡小,而后分發(fā)。
發(fā)展
針對(duì)以上的一些問(wèn)題饵史,其實(shí)我們能get到一些關(guān)鍵特性满钟,收口、分發(fā)胳喷。
OK,既然關(guān)鍵信息已經(jīng)得到湃番,偽代碼很快就能敲出:
public static void jump(String rule){
if(rule){
gotoBusinessActivity()
return;
}
if(rule){
invokeBusinessFunction();
return;
}
}
Perfect!!! 調(diào)用方便,確實(shí)做到了 收口吭露、分發(fā)
但是隨著業(yè)務(wù)發(fā)展吠撮,弊端顯示的也越發(fā)明顯。仔細(xì)分析過(guò)后讲竿,可以總結(jié)出一些問(wèn)題
膨脹
- 隨著業(yè)務(wù)的增長(zhǎng)泥兰,膨脹的太厲害弄屡,成千上萬(wàn)行代碼坐落在這個(gè)類中,名副其實(shí)的大雜燴
維護(hù)成本
- 所有項(xiàng)目均同時(shí)維護(hù)鞋诗,一個(gè)項(xiàng)目改錯(cuò)可能導(dǎo)致其他項(xiàng)目也受到影響
-
if-else
的邏輯維護(hù)太復(fù)雜 - 分發(fā)邏輯是 hardcode 膀捷,一旦錯(cuò)誤將無(wú)法進(jìn)行修改
- 各項(xiàng)目無(wú)法真正分離
擴(kuò)展
- 從代碼結(jié)構(gòu)不難看出,這里的分發(fā)實(shí)質(zhì)上是'親力親為'并非真正的分發(fā)削彬,而‘親力親為’是做不到真正的拆分的
從上面幾個(gè)問(wèn)題來(lái)看全庸,其實(shí)收口并不難,如何做好分發(fā)這才是一件難事融痛。
回顧需求與現(xiàn)有的問(wèn)題壶笼,覺(jué)得自己的if-else
模型其實(shí)丟了一件事:沒(méi)有抽離出一套統(tǒng)一的流程
那流程怎么定義?
我在設(shè)計(jì)的時(shí)候喜歡類比現(xiàn)實(shí)生活雁刷,而Router也不外乎覆劈。而這個(gè)模型就很有趣了
我想要寄快遞
這個(gè)是我需求,那么該怎么辦呢沛励?最重要的兩點(diǎn)
- 地址
- 包裹
我只需要給到快遞員地址以及包裹墩崩,那么就會(huì)給我送到(除非地址錯(cuò)了),但是這里要強(qiáng)調(diào)一點(diǎn)侯勉,我寄快遞鹦筹,并不是快遞員來(lái)接受這個(gè)包裹≈访玻快遞員只是送铐拐,而真正接收的是所在地址的人。
想到這里其實(shí)就可以梳理出Router的核心生命周期與非核心生命周期了练对。當(dāng)Router把調(diào)用者的意圖轉(zhuǎn)達(dá)給接受者時(shí)就已經(jīng)完成了它的周期. 接下來(lái)的事就是接受者干的了蚓让,跟Router無(wú)關(guān)
到這里其實(shí)Router的基本框架已經(jīng)確定(因?yàn)榱鞒桃呀?jīng)抽離), 那么剩下的就是具體模塊的事了埋酬。不難分析出Router的兩大核心模塊:規(guī)則處理、執(zhí)行器
規(guī)則
通過(guò)模型,我們知道了兩個(gè)必須的東西:地址框全、包裹 ,那對(duì)應(yīng)到程序中無(wú)非就是 目標(biāo)指向坟桅、數(shù)據(jù) 繁莹,再契合到我們當(dāng)前的業(yè)務(wù)場(chǎng)景粘秆,規(guī)則不難提煉出來(lái):
是不是很眼熟?其實(shí)就是我們通用url的簡(jiǎn)化版本下隧。其實(shí)重點(diǎn)就三個(gè)信息:
- 協(xié)議奢人,用于版本區(qū)分、功能區(qū)分等
- 目標(biāo)指向淆院,路徑何乎,意圖的接受者
- 數(shù)據(jù),攜帶的數(shù)據(jù)
同樣我們的規(guī)則定義為
協(xié)議定義完畢了,但是難點(diǎn)來(lái)了: Path的映射如何處理
舉個(gè)例子,Path 是 hotel/list
支救,但是這個(gè)只是一個(gè)字符串抢野,我真正想要調(diào)用的是 酒店列表 HotelListAction
,如何將 Path 與 目標(biāo)指向 進(jìn)行關(guān)聯(lián)各墨,這是一個(gè)問(wèn)題蒙保。在討論的過(guò)程中,其實(shí)有三種方案:
-
if-else
直接邏輯關(guān)聯(lián) - 使用
Map
進(jìn)行關(guān)聯(lián) - 使用 xml 進(jìn)行關(guān)聯(lián)
其實(shí)從本質(zhì)上來(lái)說(shuō)欲主,都一樣,都是為了數(shù)據(jù)關(guān)聯(lián)逝嚎。但是在維護(hù)扁瓢、擴(kuò)展上是有區(qū)別的。
if-else
這個(gè)不用說(shuō)补君,直接的引用會(huì)產(chǎn)生的非常復(fù)雜的依賴網(wǎng)引几,而且無(wú)法抽離出分發(fā)這塊,所有的分發(fā)都需要在當(dāng)前環(huán)境下進(jìn)行關(guān)聯(lián)挽铁。
Map
& XML
的邏輯很像伟桅,都是 path 與 處理類 的對(duì)應(yīng)。但是在維護(hù)和擴(kuò)展性上我們最終選擇了xml, 優(yōu)點(diǎn)很明顯:無(wú)代碼耦合叽掘、可動(dòng)態(tài)更新 楣铁。而MAP
的優(yōu)點(diǎn)在于,不需要進(jìn)行數(shù)據(jù)加載更扁,節(jié)省了這一部分的性能盖腕。
執(zhí)行器
執(zhí)行器的概念就好理解多了,當(dāng)一段規(guī)則被解析成可讀的意圖后浓镜,我需要將意圖溃列、數(shù)據(jù)傳遞給接受者,而這個(gè)接受者可能是各個(gè)業(yè)務(wù)所處理的膛薛。因此我需要設(shè)計(jì)一個(gè)接口(不然沒(méi)指向性了)來(lái)進(jìn)行接收听隐,而業(yè)務(wù)部門可通過(guò)實(shí)現(xiàn)這個(gè)接口來(lái)進(jìn)行處理相關(guān)邏輯。
雖說(shuō)到這里流程基本已經(jīng)結(jié)束哄啄,但是在實(shí)際的業(yè)務(wù)邏輯中會(huì)衍生出一些特殊的邏輯雅任,比如:
我要打開酒店列表,但是有個(gè)要求咨跌,必須是登陸狀態(tài)才可直接進(jìn)入椿访,否則需要先去登陸。
可能僅僅從這一個(gè)需求上來(lái)看虑润,用現(xiàn)在的流程去完成也不難成玫,可以使用兩個(gè)接收者去完成。但是當(dāng)中間衍生的特殊邏輯很多而且可能多個(gè)業(yè)務(wù)都會(huì)需要,那用兩個(gè)(多個(gè))接收者可能就有會(huì)膨脹的很厲害了哭当。那這個(gè)時(shí)候其實(shí)另外一個(gè)概念就產(chǎn)生了 -- 攔截器猪腕。
攔截器
攔截器是當(dāng)一個(gè)規(guī)則產(chǎn)生最終作用前進(jìn)行的一些特殊而通用的邏輯處理。
處理邏輯其實(shí)很像View Touch事件钦勘,當(dāng)一個(gè)Touch事件產(chǎn)生后陋葡,首先是分配(分發(fā)),然后是攔截彻采,最后才是消費(fèi)腐缤。
而我們的攔截器稍有不同的地方是
- 攔截器可以多個(gè)
- 只有當(dāng)所有攔截器全部滿足(pass)的情況下才會(huì)走到執(zhí)行器(消費(fèi))
- 這種攔截器模型可以通過(guò)遞歸來(lái)進(jìn)行實(shí)現(xiàn)
拓展
對(duì)于Router框架設(shè)計(jì)到使用到現(xiàn)在已經(jīng)2年多了,業(yè)務(wù)需求等也都基本可以完成肛响、滿足岭粤。也確實(shí)做到了Router框架抽離、流程確定且業(yè)務(wù)邏輯(包括映射關(guān)系)各自維護(hù)特笋。當(dāng)一個(gè)業(yè)務(wù)需求產(chǎn)生時(shí)剃浇,只需要產(chǎn)生一個(gè)新的規(guī)則來(lái)維護(hù)即可。
但是重新審視下目前的框架其實(shí)仍然有很多需要完善的:
- 數(shù)據(jù)映射是通過(guò)xml來(lái)映射猎物,那么必然會(huì)帶來(lái)
I/O
加載的性能開銷 - 是否項(xiàng)目調(diào)用需要寫一段很冗長(zhǎng)的規(guī)則虎囚,比如
jump("tctclient://hotel/list")
,而這種字符串參數(shù)通常是無(wú)法通過(guò)編譯器去檢查正確與否的
針對(duì)這兩個(gè)問(wèn)題,其實(shí)我們采用的是(插件)工具去完成蔫磨。
Gradle Plugin + Freemarker
在編譯時(shí)進(jìn)行輔助類的建立
- xml的加載可以在編譯時(shí)直接生成Map關(guān)系維護(hù)淘讥。
-
冗長(zhǎng)規(guī)則則可以通過(guò)枚舉來(lái)進(jìn)行維護(hù)(枚舉通過(guò)xml生成), 調(diào)用方式則改為
jump(Bridge.HOTEL_LIST)
總結(jié)
該篇文章更多的寫的是我在接受到這個(gè)需求時(shí)的心路歷程,每個(gè)人在寫代碼的時(shí)候都有自己的感受堤如。而從設(shè)計(jì)Router中其實(shí)能發(fā)現(xiàn)很多道理
沒(méi)有一個(gè)模型放之四海而皆準(zhǔn)的
比如一開始的 if-else
,當(dāng)業(yè)務(wù)量很小的時(shí)候 , 這可能是一個(gè)非常好的處理方式适揉。而當(dāng)業(yè)務(wù)膨脹,當(dāng)前結(jié)構(gòu)不適用時(shí)需及時(shí)重構(gòu)煤惩。
框架脫離了業(yè)務(wù)那就什么都不是嫉嘀,只有契合業(yè)務(wù)的框架,才是一個(gè)好框架魄揉。
思考比編碼更重要
當(dāng)接受到一個(gè)需求后剪侮,更多的是需要去思考,去設(shè)計(jì)洛退。提煉出核心關(guān)鍵點(diǎn)瓣俯、流程,針對(duì)流程梳理出核心生命周期與非核心生命周期很重要