作者介紹
馮宇飛 趾徽,現(xiàn)任人人車Android客戶端架構(gòu)師秸谢。
本文回顧總結(jié)了人人車公司Android客戶端的架構(gòu)演進歷程沈跨。人人車App隨著公司在業(yè)務(wù)和規(guī)模上的飆升,持續(xù)集成業(yè)務(wù)需求的同時捆愁,架構(gòu)也不斷的重構(gòu)演化割去,從模塊化,分層化牙瓢,到框架化劫拗,服務(wù)化,對Android客戶端架構(gòu)設(shè)計和改進有一定的參考意義矾克。
前言
對于大多數(shù)創(chuàng)業(yè)公司而言页慷, 初版開發(fā)時采用的簡單架構(gòu),在歷經(jīng)數(shù)次快速迭代后胁附,已經(jīng)成為了一個”大泥球”(源于Brian Footer和Joseph Yonder的論文《大泥球》, 定義: 一大片隨意構(gòu)造酒繁,雜亂無章,凌亂控妻,任意拼接州袒,毫無頭緒的代碼叢林), 如下問題存在于當(dāng)前的架構(gòu)中:
業(yè)務(wù)邏輯混雜在平臺實體中,造就了代碼量龐大的Activity和Fragment弓候。
本應(yīng)是全局級別獨立存在的功能模塊郎哭,卻被封鎖在某個特定視圖領(lǐng)域內(nèi),生命周期和模塊層級規(guī)劃不當(dāng)菇存。
功能拆分不夠細致和代碼重復(fù)夸研,在模塊和函數(shù)級別均有體現(xiàn)。
部分模塊之間高度耦合依鸥,使用沒有經(jīng)過合理規(guī)劃的接口來實現(xiàn)通信亥至。
部分第三方庫沒有做隔離,造成和第三方庫的高度耦合贱迟,還存在庫的誤用和濫用姐扮。
分層化缺失,領(lǐng)域邊界基本不存在衣吠,比如數(shù)據(jù)層和業(yè)務(wù)層共享同一套數(shù)據(jù)結(jié)構(gòu)茶敏。
缺乏內(nèi)部規(guī)范,部分概念或?qū)嶓w的表述混亂零碎缚俏,作為workaround的謎之代碼比較多惊搏。
總之,初始搭建的架構(gòu)已經(jīng)不足以支撐長期的持續(xù)性開發(fā)袍榆,泥球最終會把開發(fā)者推入代碼深淵胀屿。重構(gòu)是必須的,但是從哪里開始需要好好考慮包雀。
筆者的觀點是宿崭,對于大中型項目,短時間內(nèi)不太可能建立起對項目需求和現(xiàn)行邏輯的大局觀才写,并且在重構(gòu)的同時還要保證項目的及時發(fā)布(想想那個經(jīng)典的比喻葡兑,為一輛高速奔馳的汽車換輪胎)奖蔓,那么從次級業(yè)務(wù)模塊進行改進和重構(gòu)是一條穩(wěn)妥之路,在重構(gòu)的同時一點一點的豐富細節(jié)認知和整體布局讹堤,最終倒逼整體架構(gòu)的變革吆鹤。
下面分為兩個階段對人人車Android客戶端架構(gòu)演進的方案和階段進行闡述:
第一階段演進
1. 業(yè)務(wù)視圖模塊
就人人車App來說,重構(gòu)前洲守,所有的子業(yè)務(wù)邏輯都寫在頁面載體中疑务,從數(shù)據(jù)提取,視圖配置到交互都混雜在一起梗醇,每個頁面都是泥球知允,導(dǎo)致作為頁面載體的Activity和Fragment體量極為龐大,代碼的閱讀和后續(xù)修改都很費勁叙谨。
上述問題在結(jié)構(gòu)設(shè)計層面上體現(xiàn)為:邊界缺失温鸽。每種子業(yè)務(wù),都應(yīng)該有邊界手负,有了邊界才有所謂的內(nèi)聚性涤垫,才能區(qū)分外部使用者和內(nèi)部實現(xiàn)者。
既然是因為邊界缺失導(dǎo)致了結(jié)構(gòu)問題竟终,那么增加邊界即可,業(yè)務(wù)視圖模塊是我們采用的解決方案蝠猬,業(yè)務(wù)功能級視圖模塊的實現(xiàn)很簡單,可以理解為代碼的類級別隔離帶來了邊界衡楞,進而從頁面載體(Activity/Fragment)剝離了業(yè)務(wù)吱雏。
上圖左半部分是人人車App的車輛詳情頁敦姻,可以看到瘾境,詳情頁的不同視圖部分對應(yīng)到了不同的Module(注意,這里僅僅是一個示例性質(zhì)的劃分)镰惦,每個Module負責(zé)了不同部分的視圖配置和業(yè)務(wù)交互(有些情況下迷守,甚至視圖生成也是由Module負責(zé)的)。頁面上通常承載復(fù)數(shù)項子業(yè)務(wù)旺入,以子業(yè)務(wù)為維度進行劃分視圖層兑凿,就得到復(fù)數(shù)個的視圖切片,于是在視圖層和業(yè)務(wù)邏輯層之間產(chǎn)生了以子業(yè)務(wù)為粒度的映射茵瘾,業(yè)務(wù)視圖模塊封裝了這類映射礼华,每個模塊封裝了特定子業(yè)務(wù)的視圖切片配置和業(yè)務(wù)邏輯,如上圖一般拗秘,詳情頁的各個子業(yè)務(wù)被不同的模塊承包圣絮。
視圖模塊本身還可以再進行拆分,如果有邏輯再分層需要的話雕旨,比如Module A在內(nèi)部又將功能分包給了SubModule A和SubModule B扮匠。業(yè)務(wù)視圖模塊的另外一個特點是不依賴于視圖的真正的布局細節(jié)捧请,比如上圖的Module F, 其負責(zé)了車輛詳情頁的對比子功能,那么棒搜,車輛標題右邊的增加對比按鈕和懸浮的對比按鈕就都是其應(yīng)該管轄的視圖片段疹蛉,而不會意味兩者處于不同的布局層就分模塊處理,這也反映了視圖模塊拆分以業(yè)務(wù)為粒度的原則力麸。
業(yè)務(wù)視圖模塊以類似于插件的形式和Activity/Fragment進行聯(lián)動可款,Activity/Fragment本身不再承載任何視圖業(yè)務(wù)邏輯,僅僅需要維護內(nèi)部寄生的模塊克蚂,同時針對Android平臺本身的Activity/Fragment生命周期特性筑舅,Activity/Fragment還需要負責(zé)將自己的生命周期回調(diào)分發(fā)給相應(yīng)的模塊,以保證某些依賴平臺生命周期特性的業(yè)務(wù)可以被封裝在模塊內(nèi)陨舱。Activity/Fragment的職責(zé)退化為模塊容器和數(shù)據(jù)媒介翠拣,數(shù)據(jù)媒介的作用體現(xiàn)在,Activity/Fragment在某些情況下會扮演頁面數(shù)據(jù)的入口和分發(fā)者游盲,如上圖下方所示误墓,Data到達Activity后,Activity要將Data再次分發(fā)給內(nèi)部那些真正需要數(shù)據(jù)的模塊益缎。
總的來看業(yè)務(wù)視圖模塊承擔(dān)了兩個職責(zé):
是對外封裝業(yè)務(wù)本身的實現(xiàn)谜慌,包括視圖配置和業(yè)務(wù)邏輯。
對內(nèi)模擬了寄生容器的某些特性(這里是生命周期)莺奔,從而使得依賴容器特性的業(yè)務(wù)也得以運行欣范。
視圖業(yè)務(wù)模塊顯得簡單直白,但是其效果和收益相當(dāng)可觀令哟,具有結(jié)構(gòu)性意義恼琼,因為這一步劃分了邊界,邊界促進了頁面結(jié)構(gòu)的明晰化和職責(zé)的封裝拆分屏富,同時限制了代碼的污染性溢出晴竞,有可能模塊內(nèi)部的實現(xiàn)還是以前的丑陋代碼,但這時候其已經(jīng)退化為一個小泥球狠半,在邊界的約束力下對架構(gòu)本身的影響力被限制在最小噩死。同時,以插件思路設(shè)計的模塊也能實現(xiàn)一定程度的代碼復(fù)用(但這一點不是模塊化的目的神年,因為業(yè)務(wù)級別的復(fù)用非常依賴產(chǎn)品和設(shè)計已维,可以看作是錦上添花)。
另外已日,模塊化帶來的副作用就是通信的問題垛耳,模塊化帶來的邊界割裂了一體化,需要額外的通信手段,這里我們增加一個Mediator來實現(xiàn)了模塊之間的通信艾扮。實際上既琴,一個良好的業(yè)務(wù)體系,子業(yè)務(wù)之間不應(yīng)該有太多的通信泡嘴,所以Mediator的復(fù)雜度和方法數(shù)并不會為因為模塊數(shù)量的增加而指數(shù)級增長甫恩。
2. 數(shù)據(jù)邊界
數(shù)據(jù)邊界的宗旨是隔離外部原始數(shù)據(jù)和內(nèi)部業(yè)務(wù)數(shù)據(jù),在數(shù)據(jù)結(jié)構(gòu)的維度實現(xiàn)邊界和職責(zé)劃分酌予,以此來緩沖外部污染數(shù)據(jù)對內(nèi)部業(yè)務(wù)邏輯的沖擊力磺箕。
我們應(yīng)該有這樣的認識:外部原始數(shù)據(jù)永遠是不可靠的。對于內(nèi)層業(yè)務(wù)邏輯來說抛虫,外部數(shù)據(jù)來源有很多種松靡,網(wǎng)絡(luò),文件建椰,數(shù)據(jù)庫等等雕欺,這些數(shù)據(jù)來源都是不可靠的,網(wǎng)絡(luò)不穩(wěn)定棉姐,服務(wù)器故障屠列,磁盤文件被誤寫都是不可避免的。如果任由這部分污染數(shù)據(jù)直接傳遞到業(yè)務(wù)層邏輯伞矩,業(yè)務(wù)層邏輯在沒有足夠防御的情況下就會運轉(zhuǎn)失常甚至于崩潰笛洛。退一步講,即使業(yè)務(wù)層邏輯做了足夠的防御乃坤,可以抑制污染數(shù)據(jù)的破壞力苛让,從職責(zé)劃分上講已經(jīng)出現(xiàn)了問題: 業(yè)務(wù)邏輯承擔(dān)了它不應(yīng)該承擔(dān)的數(shù)據(jù)防御職責(zé)。另一個設(shè)計上的缺陷是:業(yè)務(wù)邏輯直接套用了外部原始數(shù)據(jù)的數(shù)據(jù)載體湿诊,這樣導(dǎo)致業(yè)務(wù)層數(shù)據(jù)處理邏輯和外部原始數(shù)據(jù)的格式等信息發(fā)生了強耦合狱杰,在數(shù)據(jù)表示層兩者沒有得到區(qū)分,這其實是一個在數(shù)據(jù)結(jié)構(gòu)上職責(zé)沒有劃分的問題枫吧。
上述設(shè)計缺陷的解決方案最終形成了數(shù)據(jù)邊界浦旱。解決方案有這么幾個要點:
數(shù)據(jù)校驗的職責(zé)從業(yè)務(wù)邏輯中剝離宇色,形成單獨的職能模塊九杂。
外部原始數(shù)據(jù)的數(shù)據(jù)載體要和業(yè)務(wù)邏輯的數(shù)據(jù)載體做到完全隔離。
所有的外部數(shù)據(jù)入口要得到有效的控制宣蠕。
上述幾點互相關(guān)聯(lián):只有控制了所有外部數(shù)據(jù)入口例隆,才能對所有外部數(shù)據(jù)做校驗,而數(shù)據(jù)載體隔離需要的數(shù)據(jù)轉(zhuǎn)換也需要依托對所有外部數(shù)據(jù)入口的控制抢蚀,數(shù)據(jù)校驗可以作為數(shù)據(jù)轉(zhuǎn)換的前置步驟镀层。數(shù)據(jù)邊界的示意圖如下:
每一個外部數(shù)據(jù)入口都有一道關(guān)卡: Data Mapper, 這就是上面第一點提到的獨立職能模塊,Data Mapper扮演外部數(shù)據(jù)運輸?shù)絻?nèi)部的關(guān)卡,Data Mapper承載了兩個職責(zé): 數(shù)據(jù)校驗和數(shù)據(jù)轉(zhuǎn)換(因為這兩個職責(zé)本身是前后關(guān)聯(lián)的唱逢,沒有必要再次進行模塊級的拆分)
數(shù)據(jù)校驗: 按照業(yè)務(wù)特性或者規(guī)范約定吴侦,對外部原始數(shù)據(jù)進行一次全面排查:如果數(shù)據(jù)在整體上已經(jīng)被污染,那么該數(shù)據(jù)會被徹底放棄坞古,但會被轉(zhuǎn)換為空數(shù)據(jù)或者約定好的無效數(shù)據(jù)(比如Java中的null)傳遞給業(yè)務(wù)層(數(shù)據(jù)到達本身也是一個信息备韧,需要業(yè)務(wù)層知曉。比如加載數(shù)據(jù)會顯示加載中界面痪枫,即使返回了污染數(shù)據(jù)织堂,也是需要停止顯示加載中界面而顯示空頁面或者錯誤頁面的,如果直接忽略此次數(shù)據(jù)的到達奶陈,那么就會一直顯示加載中)如果數(shù)據(jù)只是部分污染易阳,那么會嘗試剔除被污染的部分,以實現(xiàn)力所能及的信息傳遞吃粒×拾常總結(jié)來說,數(shù)據(jù)校驗將數(shù)據(jù)洗白徐勃,保證數(shù)據(jù)的安全性黑竞,至于數(shù)據(jù)的功能性則由數(shù)據(jù)轉(zhuǎn)換來保證。
數(shù)據(jù)轉(zhuǎn)換: 相對數(shù)據(jù)校驗要復(fù)雜一些疏旨,需要根據(jù)外部原始數(shù)據(jù)組裝出可用的業(yè)務(wù)層數(shù)據(jù)很魂,除了單純的數(shù)據(jù)結(jié)構(gòu)調(diào)整外,還有可能會有數(shù)據(jù)裁剪檐涝,數(shù)據(jù)拼接等復(fù)合操作遏匆。數(shù)據(jù)轉(zhuǎn)換還有一個作用是保持整體業(yè)務(wù)數(shù)據(jù)的規(guī)范性,比如對于車輛價格這類數(shù)據(jù)谁榜,在業(yè)務(wù)層面會對有一個精度方面的規(guī)范約束(最多兩位小數(shù)), 如果得到的數(shù)據(jù)沒有遵循這個規(guī)范幅聘,我們稱其為違規(guī)數(shù)據(jù),違規(guī)數(shù)據(jù)和污染數(shù)據(jù)是有區(qū)別的窃植,前者可以在數(shù)據(jù)轉(zhuǎn)換中得到矯正帝蒿,從而保證了傳遞給業(yè)務(wù)層的數(shù)據(jù)的規(guī)范性。
如上圖中Data Mapper示意圖所示: 通過數(shù)據(jù)校驗和數(shù)據(jù)轉(zhuǎn)換的協(xié)力巷怜,各種各樣正常的葛超,殘缺的,污染的延塑,違規(guī)的外部原始數(shù)據(jù)最終被轉(zhuǎn)化為安全的绣张,規(guī)范的,可用的業(yè)務(wù)層數(shù)據(jù)关带。數(shù)據(jù)邊界猶如一個數(shù)據(jù)沙盒侥涵,在業(yè)務(wù)層來看,它感知到的是一個絕對可靠的業(yè)務(wù)數(shù)據(jù)環(huán)境。
數(shù)據(jù)邊界的思想內(nèi)核其實是分層化芜飘,職責(zé)下放到不同的層务豺,每一層向上一層提供特定的服務(wù),上一層不必關(guān)心下面的細節(jié)嗦明,每一層功能和職責(zé)專一化冲呢。
3. 引入RxJava
先談一下筆者對第三方庫的使用觀點:
謹慎,克制的引入新庫招狸,加法容易減法難敬拓,庫越多,沖突和妥協(xié)就越多裙戏,開發(fā)和發(fā)布成本也會增加乘凸。
隨著業(yè)務(wù)本身的不斷演化,基于通用性目的設(shè)計的庫遲早會和特定業(yè)務(wù)需求之間產(chǎn)生沖突累榜。
組件級的庫要比框架級的庫安全营勤。因為前者只封裝了單點功能,后者則封裝了流程壹罚。
在引入或者替換庫前要清楚你的需求和要付出的代價葛作,不要追逐風(fēng)潮,人云亦云猖凛。
經(jīng)過對響應(yīng)式編程的學(xué)習(xí)理解和RxJava庫的源碼解析赂蠢,我們引入了RxJava以期以下收益:
Observable實現(xiàn)了回調(diào)方式的歸一化。
Operator讓回調(diào)的傳遞和處理靈活而富有組合性辨泳。
線程調(diào)度非常方便虱岂。
從實際應(yīng)用上講,RxJava基于信息流模型對一些常見操作和場景進行了模擬封裝菠红,使得開發(fā)快捷第岖,實現(xiàn)優(yōu)雅,如:
Observable模擬了一條單向信道试溯。
Subject模擬了一個自發(fā)信息源蔑滓。
BehaviorSubject作為Subject的擴展模擬了一個會緩存上次發(fā)出信息的信息源。
Map模擬了形態(tài)切換遇绞。
FlatMap模擬了復(fù)雜結(jié)構(gòu)信息的降維键袱。
Merge模擬了信息流的合并。
CombineLatest和Zip模擬了信息流的同步试读。
等等不再贅述杠纵。
上述功能和特性在我們的整個開發(fā)和重構(gòu)過程中發(fā)揮了很大的助力,讓實現(xiàn)更加簡潔優(yōu)雅钩骇。本文在這里不再做更多展開,在后面的重構(gòu)項目中會穿插介紹對Rxjava特性或功能的使用。
4. 數(shù)據(jù)源和數(shù)據(jù)隧道
數(shù)據(jù)源和數(shù)據(jù)隧道是我們對提取數(shù)據(jù)-刷新頁面這個常規(guī)流程的重新建模倘屹,以適應(yīng)人人車App的頁面刷新場景: 人人車App是一個重呈現(xiàn)的App银亲,比如“我要買車”頁面,用戶在提交篩選條件后纽匙,需要從服務(wù)器獲取數(shù)據(jù)然后刷新頁面务蝠,將新的數(shù)據(jù)呈現(xiàn)給用戶,顯然烛缔,這個頁面會有非常頻繁的提取數(shù)據(jù)-刷新頁面行為馏段。
上圖上半部分描述了之前我們對 提取數(shù)據(jù)-刷新頁面 這一流程的建模: Module(界面)向Data Fetcher發(fā)起fetch調(diào)用,在調(diào)用中要傳遞一個回調(diào)践瓷,在Data Fetcher獲取新數(shù)據(jù)以后院喜,執(zhí)行回調(diào)來觸發(fā)Module刷新界面。這個模型在初期可以正常工作晕翠,不過在使用上已經(jīng)暴露了其笨重的地方喷舀,每次想要請求新數(shù)據(jù)時,都必須攜帶一個回調(diào)淋肾,每次的請求數(shù)據(jù)和刷新都是一次性行為硫麻,使用起來比較麻煩。到了中期樊卓,新的需求出現(xiàn)了:在本Module之外的其他操作(比如用戶在其他Module的行為)也要能觸發(fā)該頁面的 提取數(shù)據(jù)-刷新頁面 流程拿愧,很顯然,當(dāng)前的one-shot模型必不能很優(yōu)雅的實現(xiàn)這個需求碌尔。
示意圖下半部分描述了我們對 提取數(shù)據(jù)-刷新頁面 的重新建模:
引入數(shù)據(jù)源(Data Source)赶掖,數(shù)據(jù)源和Data Fetcher的區(qū)別在于,數(shù)據(jù)源是獨立存在的主動數(shù)據(jù)提供者七扰,Data Fetcher則只能算是一個功能性的包裝奢赂。數(shù)據(jù)源獨立存在的意義在于將其從頁面Module的功能性附屬(Data Fetcher其實就是一個功能性附屬,只提供了靜態(tài)級功能颈走,沒有自己的核)中剝離為單獨的實體膳灶,從此數(shù)據(jù)是數(shù)據(jù),頁面是頁面立由。數(shù)據(jù)源作為主動數(shù)據(jù)提供者體現(xiàn)在它可以提供數(shù)據(jù)隧道給使用者轧钓。
引入數(shù)據(jù)隧道(Data Tunnel), 數(shù)據(jù)隧道本質(zhì)上講很簡單锐膜,就是一個固定的觀察者罷了毕箍,但是從設(shè)計角度看,它有截然不同的意義。它將原來主動“拉”新數(shù)據(jù)轉(zhuǎn)換為了數(shù)據(jù)源“推”新數(shù)據(jù)過來透绩,這樣就可以滿足我們在上面遇到的新需求了,頁面只需要建立一條到數(shù)據(jù)源的數(shù)據(jù)隧道道伟,在外界觸發(fā)了數(shù)據(jù)源的更新后媒咳,數(shù)據(jù)源會主動的將新數(shù)據(jù)通過數(shù)據(jù)隧道推到頁面粹排。上圖展示了這個流程以及其擴展: 一個數(shù)據(jù)源可以同復(fù)數(shù)個使用者建立數(shù)據(jù)隧道,只要數(shù)據(jù)源更新了數(shù)據(jù)(注意涩澡,這個數(shù)據(jù)源本身不具有自主更新的能力顽耳,需要外界來觸發(fā),比如上圖的Module或者Trigger)就會將新數(shù)據(jù)推送到所有的數(shù)據(jù)對端妙同。使用者不再需要數(shù)據(jù)隧道時可以方便的進行拆卸銷毀射富。
數(shù)據(jù)源-數(shù)據(jù)隧道模型建立得益于RxJava的BehaviorSubject,實現(xiàn)的非常優(yōu)雅粥帚,并且還解決了一個數(shù)據(jù)界面同步的問題胰耗,比如會有這樣的場景:
頁面啟動和數(shù)據(jù)到來之間沒有固定的先后順序,頁面啟動完畢后需要數(shù)據(jù)茎辐,如果數(shù)據(jù)之前沒有到來宪郊,那么頁面等待數(shù)據(jù)到來即可,但是如果之前數(shù)據(jù)已經(jīng)到來拖陆,鑒于數(shù)據(jù)到來時頁面還沒有啟動完畢弛槐,感知不到數(shù)據(jù)到來,所以需要數(shù)據(jù)源能夠緩存上一次成功發(fā)出的數(shù)據(jù)依啰,BehaviorSubject的特性完美的契合了這個場景: BehaviorSubject內(nèi)部會緩存消息流的最近一個條息, 在后續(xù)有Subscriber訂閱時乎串,會直接將緩存的消息投遞給Subscriber。另外RxJava的onTerminateDetach優(yōu)雅的處理了銷毀數(shù)據(jù)隧道時的內(nèi)存泄漏風(fēng)險速警。
數(shù)據(jù)源-數(shù)據(jù)隧道模型化被動為主動叹誉,適應(yīng)需求的同時也在架構(gòu)上反映了數(shù)據(jù)層獨立的趨勢。
5. 數(shù)據(jù)源的分層和組合
該重構(gòu)是上一步數(shù)據(jù)源模型的延伸擴展闷旧,在數(shù)據(jù)層進行的一次增強长豁。
考慮這樣的應(yīng)用場景:數(shù)據(jù)源A提供的數(shù)據(jù)被模塊B使用,數(shù)據(jù)源C提供的數(shù)據(jù)被模塊D使用忙灼,有了新的需求導(dǎo)致數(shù)據(jù)源C的部分數(shù)據(jù)替換為數(shù)據(jù)源A提供的數(shù)據(jù)匠襟,這個時候有兩種需求實現(xiàn)思路:
模塊D直接使用數(shù)據(jù)源A和數(shù)據(jù)源C,但這樣必然導(dǎo)致模塊D邏輯代碼的改動该园,除了要引入數(shù)據(jù)源A之外酸舍,還需要對原先處理數(shù)據(jù)的邏輯進行修改來適應(yīng)這個變化。和我們希望的最理想方案有差距:理想方案是是模塊D不需要修改里初,因為從本質(zhì)上講啃勉,模塊D的功能在新需求中沒有任何變化,這次的變化是一個數(shù)據(jù)層的變化双妨,不應(yīng)該讓其影響數(shù)據(jù)層之上的模塊淮阐,模塊不應(yīng)該承擔(dān)數(shù)據(jù)整合的職責(zé)叮阅。
數(shù)據(jù)源A和數(shù)據(jù)源C進行組合,數(shù)據(jù)源C作為和模塊D對口的數(shù)據(jù)源不變枝嘶,這樣就可以保證模塊D的數(shù)據(jù)提取邏輯不變帘饶,為了適應(yīng)新的數(shù)據(jù)需求哑诊,數(shù)據(jù)源C化身為數(shù)據(jù)源A的一個使用者群扶,和數(shù)據(jù)源A建立數(shù)據(jù)通道后,就能獲取數(shù)據(jù)源A的數(shù)據(jù)以及對A數(shù)據(jù)變化的感知镀裤,有了A的數(shù)據(jù)竞阐,就可以在內(nèi)部使用A的數(shù)據(jù)替換自己內(nèi)部要被替換的部分,經(jīng)過聚合的數(shù)據(jù)就是滿足這次需求的數(shù)據(jù)暑劝。
顯然第二種方式在結(jié)構(gòu)和職責(zé)上更加合理骆莹,而這種對數(shù)據(jù)源A和C的組合應(yīng)用就是是數(shù)據(jù)源功能擴展的一個例子。
剛才的例子展示了數(shù)據(jù)源之間組合的靈活性担猛,出于內(nèi)部規(guī)范的要求幕垦,我們對數(shù)據(jù)源進行了一次概括性分層,如上圖所示分為兩層傅联,業(yè)務(wù)級數(shù)據(jù)源和單點數(shù)據(jù)源:
單點級數(shù)據(jù)源和服務(wù)器接口等外部數(shù)據(jù)提供者一一對應(yīng)先改,功能是最純粹的,就是外部數(shù)據(jù)提供者在架構(gòu)中的化身封裝蒸走,只提供單一類型的數(shù)據(jù)仇奶,如上圖的Data Source A,B,C,D。
業(yè)務(wù)級數(shù)據(jù)源一般自己沒有產(chǎn)生數(shù)據(jù)的能力比驻,其作用主要體現(xiàn)在對單點級數(shù)據(jù)源的聚合和管理上该溯,只對接業(yè)務(wù)級模塊。該類數(shù)據(jù)源的引入是作為一個中間層來抹平實際數(shù)據(jù)和業(yè)務(wù)數(shù)據(jù)需求之間的溝壑别惦,如上圖的Data Source E,F, 兩者的數(shù)據(jù)生成依賴于Data Source A, B, C狈茉,但是A, B, C的數(shù)據(jù)也需要經(jīng)過E, F的適配才能滿足Module的數(shù)據(jù)需求。
數(shù)據(jù)源組合和涉及到一個數(shù)據(jù)同步的問題: 以Data Source F為例掸掸,F(xiàn)依賴于Data Source A和B的數(shù)據(jù)氯庆,F(xiàn)只有在A和B的數(shù)據(jù)都就緒的情況下,才能構(gòu)造完整的業(yè)務(wù)數(shù)據(jù)進行投遞猾漫,具體的細節(jié)如上圖下半部分所示:在A和B的數(shù)據(jù)沒有全部到位時点晴,F(xiàn)不會發(fā)送數(shù)據(jù) ,后續(xù)A和B有任何數(shù)據(jù)更新悯周,F(xiàn)也需要同步的刷新數(shù)據(jù)并發(fā)送(該圖參考了ReactX的CombineLatest示意圖)粒督,得益于RxJava的CombineLatest功能,數(shù)據(jù)同步的實現(xiàn)極為簡便禽翼。業(yè)務(wù)層數(shù)據(jù)源對業(yè)務(wù)模塊屏蔽了數(shù)據(jù)源聚合的細節(jié)屠橄,作為中間層出色的完成了任務(wù)族跛。
分層和組合還有一些要點是,業(yè)務(wù)級數(shù)據(jù)源之間可以也可以組合以適應(yīng)需求锐墙,前面所說的兩層是一個概念上的分層礁哄,實際實現(xiàn)中可以嵌套多層,一個業(yè)務(wù)級數(shù)據(jù)源在更上一層來看也可以認為是一個單點級數(shù)據(jù)源溪北,只要在數(shù)據(jù)的分布上合理即可桐绒。另外業(yè)務(wù)模塊不強制使用業(yè)務(wù)級數(shù)據(jù)源,因為某些業(yè)務(wù)所需要的數(shù)據(jù)一個單點數(shù)據(jù)源足以覆蓋之拨,沒有必要引入多余層茉继,如上圖的Module D就直接使用了單點數(shù)據(jù)源 D。
數(shù)據(jù)源的分層和組合解決了實際服務(wù)端接口提供的數(shù)據(jù)和業(yè)務(wù)場景需要的數(shù)據(jù)之間的矛盾蚀乔,可能會覺得這和數(shù)據(jù)邊界的Data Mapper功能重疊烁竭,但其實兩者處于不同的數(shù)據(jù)層次,業(yè)務(wù)級數(shù)據(jù)源的數(shù)據(jù)整合生成可以被認為是數(shù)據(jù)源內(nèi)部的數(shù)據(jù)整合吉挣,而Data Mapper則是數(shù)據(jù)源外部的下游數(shù)據(jù)加工者派撕。
6. 通用功能的聚合
通用功能的聚合是重構(gòu)之路的必經(jīng)階段,將一個通用功能原來零散分布在代碼中的實現(xiàn)全部聚合提取為單獨的功能性模塊睬魂,是一次功能級邊界的確立终吼,下圖是一個簡單示意:
人人車App的通用功能聚合涉及的功能較多,比如車輛篩選汉买,砍價衔峰,預(yù)約等特定業(yè)務(wù)功能。這里大致總結(jié)下聚合的思路和手法: 全局的歸全局蛙粘,局部的歸局部垫卤,功能聚合為功能模塊。
功能聚合之前的現(xiàn)狀是: 一些功能在實現(xiàn)時局限于產(chǎn)品設(shè)計出牧,被限制在特定的頁面子功能中穴肘,比如車輛篩選被限制在“我要買車”頁中,砍價被限制在“車輛詳情頁”中舔痕,究其原因评抚,是因為這個功能在提出時只存在于某個頁面中,這樣在實現(xiàn)時也不由的被限制了思路伯复。但是慨代,以砍價功能為例,從領(lǐng)域劃分上講啸如,砍價功能和“車輛詳情頁”不在一個領(lǐng)域內(nèi)侍匙,兩者之間只有簡單的使用關(guān)系罷了,從變化上講叮雳,砍價功能和“車輛詳情頁”不是緊耦合的想暗,在其他可以提供足夠信息的頁面妇汗,砍價功能理論上都可以被加上(后面果然在其他的頁面增加了此功能)。因此砍價功能本身聚合為一個功能性模塊是由必要的说莫,在代碼上避免了代碼重復(fù)杨箭,也有了自己的領(lǐng)域。
上述思路是進行功能聚合的一個指導(dǎo)思想储狭,在被提升為全局功能模塊后互婿,使用者只需創(chuàng)建功能模塊,然后使用即可晶密,不過對于一些依賴于Android生命周期的功能擒悬,使用者還需要保證生命周期回調(diào)模她。
通用功能聚合和前面的業(yè)務(wù)視圖模塊類似稻艰,兩者的思路一致,不同的是處于的領(lǐng)域和獨立性侈净。
第一階段演進總結(jié)
業(yè)務(wù)級頁面得益于業(yè)務(wù)視圖模塊尊勿,在內(nèi)部細節(jié)層面已經(jīng)變的邊界分明,結(jié)構(gòu)清晰畜侦,部分視圖模塊得到了高度復(fù)用元扔。
數(shù)據(jù)邊界使得外界非法數(shù)據(jù)對內(nèi)部邏輯基本不能造成影響,數(shù)據(jù)魯棒性得到了提高旋膳,并且進一步的業(yè)務(wù)數(shù)據(jù)體系和外部數(shù)據(jù)體系實現(xiàn)了完全隔離澎语,盡管有所冗余,但是卻為了兩端數(shù)據(jù)結(jié)構(gòu)的靈活變化留夠了余地验懊,數(shù)據(jù)預(yù)處理和規(guī)范化職責(zé)也被明確的抽取到專門的實體擅羞,數(shù)據(jù)層內(nèi)部的邊界明確,功能粒度細化义图。
數(shù)據(jù)源及其擴展初步構(gòu)建了獨立的數(shù)據(jù)層减俏,新的 提取數(shù)據(jù)-刷新頁面 模型很好的適應(yīng)了數(shù)據(jù)頁面之間新的聯(lián)動需求,數(shù)據(jù)源之間的靈活組合也提供了客戶端對服務(wù)端接口變化的良好適應(yīng)力碱工。
通用功能聚合娃承,減少代碼重復(fù),加快了項目的開發(fā)怕篷,確立了通用功能的層次和邊界历筝。
總結(jié): 第一階段的重構(gòu)偏向于模塊化和層次化,多是對一些次級領(lǐng)域進行改進廊谓。
第二階段演進
7. 錨點系統(tǒng)
錨點系統(tǒng)引入的初衷梳猪,是為了得到當(dāng)前顯示在前臺的Activity,充其量是一個Activity全局信息維護系統(tǒng)蹂析,不過隨著后續(xù)的持續(xù)強化擴展舔示,潛力被慢慢發(fā)掘碟婆,最終演化為骨干級的系統(tǒng)框架。先闡述一下錨點的概念: Android的特性決定了大多數(shù)視圖相關(guān)操作都需要Activity的介入惕稻,比如顯示一個對話框竖共,必須要提供一個Activity才能進行顯示,Activity的角色就是錨點俺祠,錨定了上下文公给,通過錨點就可以獲得需要的上下文信息,從而進行基于上下文的操作蜘渣。
Android中有很常見的異步操作場景淌铐,該異步操作在執(zhí)行過程中會需要一個Activity,常規(guī)的思路就是讓異步操作持有Activity蔫缸,不過限于異步操作會導(dǎo)致activity內(nèi)存延遲釋放甚至泄露腿准,需要使用一定手段來進行規(guī)避,MVP拾碌,弱引用等都是解決方案吐葱。不過MVP在使用上不夠靈活,弱引用則不夠優(yōu)雅校翔,因此引入了錨點系統(tǒng)來提供一個更好的解決方案弟跑。
錨點系統(tǒng)的思路是在系統(tǒng)內(nèi)部通過獨一無二的輕量級標識(一個數(shù)值類標識, PageId)來對應(yīng)和識別Activity/Fragment等系統(tǒng)界面組件。外界使用者對界面組件的引用使用輕量級標識來避免直接引用Activity/Fragment防症。好比每個Activity/Fragment都會在錨點系統(tǒng)內(nèi)登記孟辑,向錨點系統(tǒng)提供其特殊的標識,外界通過此標識借助錨點系統(tǒng)即可獲得對應(yīng)的Activity/Fragment實體蔫敲,另外饲嗽,鑒于Activity/Fragment擁有自己的生命周期,因此Activity/Fragment在自己的生命周期回調(diào)中都需要通知錨點系統(tǒng)燕偶,錨點系統(tǒng)根據(jù)中這些回調(diào)動態(tài)增刪添改內(nèi)部維護的實體信息喝噪,外部如果使用一個已經(jīng)銷毀的界面組件的標識會被告知標識已經(jīng)無效。
我們在規(guī)劃錨點系統(tǒng)時指么,除了賦予其上面描述的頁面組件標識功能外酝惧,還通過制定Activity/Fragment頁面規(guī)范來支撐了錨點系統(tǒng)的當(dāng)前展示頁面信息:
先引入PageView接口,作為業(yè)務(wù)級頁面的表征伯诬,如上圖右下角的人人車App“車輛詳情頁”: Android的視圖展現(xiàn)一般都是采用Activity或者Activity內(nèi)部使用Fragment來實現(xiàn)晚唇,Activity/Fragment各代表了不同層的業(yè)務(wù)頁面,在這個例子中盗似,F(xiàn)ragment D代表的是“車輛詳情頁”PageView哩陕,但是車輛詳情頁本身有可以被細分為不同的領(lǐng)域,F(xiàn)ragment F代表的是“車輛詳情頁”的“車輛參數(shù)”PageView,Activity C本身只作為Fragment的載體悍及,是一個“無形”的PageView闽瓢。在上圖中,當(dāng)前“呈現(xiàn)”的PageView是“車輛參數(shù)”心赶。
錨點系統(tǒng)的另一個功能就是可以自動維護當(dāng)前呈現(xiàn)的PageView信息扣讼,會對外提供一個查詢接口來返回當(dāng)前呈現(xiàn)的是哪個PageView,以及其對應(yīng)的業(yè)務(wù)頁面的類型缨叫,這個功能可以簡化一些依賴當(dāng)前展示界面類型的功能的實現(xiàn)椭符,比如,在按鈕點擊的統(tǒng)計參數(shù)上報中需要增加當(dāng)前處于哪個頁面耻姥,如果沒有錨點系統(tǒng)提供獨立的查詢服務(wù)销钝,就要在在每個按鈕點擊上報的邏輯寫死按鈕處于的頁面類型,而借助于錨點系統(tǒng)琐簇,只需要在提交上報請求時統(tǒng)一補充上當(dāng)前頁面類型即可蒸健,因為按鈕點擊上報時,當(dāng)前呈現(xiàn)的頁面一定是按鈕所在的頁面鸽嫂。
以上圖來系統(tǒng)性的說明錨點系統(tǒng)如何運作纵装,其描述了這樣的場景:
用戶退出Activity A進入Activity B(Activity B內(nèi)部則包含了Fragment A,E),在從Activity B進入了Activity C(包含了Fragment C,D,F,G),用戶繼續(xù)進行操作据某,馬上會進入Activity D。
首先上面的Activity/Fragment都以PageView的形態(tài)(即實現(xiàn)了PageView接口)注冊在錨點系統(tǒng)中诗箍,在Activity/Fragment創(chuàng)建和銷毀時會自動登記和釋放癣籽。除了創(chuàng)建和銷毀,其他的界面組件生命周期回調(diào)也會被通告給錨點系統(tǒng)滤祖,錨點系統(tǒng)根據(jù)這些變化維護當(dāng)前哪個PageView是處于前臺的筷狼。如上圖所示的那樣,Activity B以及其內(nèi)部包含的Fragment都被切到了后臺匠童,處于凍結(jié)狀態(tài)埂材。
此時在前臺的是Activity C和其包含的Fragment,但僅僅知道這一步不足以細化到業(yè)務(wù)頁面層面汤求,還需要在Activity C和Fragment集合中尋找真正在前臺的PageView俏险,錨點系統(tǒng)內(nèi)部將Activity/Fragment按照預(yù)設(shè)的層級進行了分層管理,Activity一般會承載Fragment, 那么如果同時存在前臺Activity和Fragment的話扬绪,前臺Fragment代表的才是真正呈現(xiàn)的PageView(比如上圖竖独,Activity C雖然處于運行狀態(tài),但是不算前臺PageView), 再進一步挤牛,F(xiàn)ragment之間也有層級劃分莹痢,如上圖的Fragment F和Fragment D, Fragment D的層級最深,也是展示在最前的頁面,那么Fragment D就勝過了Fragment F成為了真正的前臺PageView竞膳,直到即將到來的Activity D切到前臺為止航瞭。
錨點系統(tǒng)還提供了對全局頁面狀態(tài)變化的監(jiān)聽服務(wù),任何實體都可以向其注冊來感知所有頁面的狀態(tài)變化坦辟。
錨點系統(tǒng)的前提是所有頁面級的Activity/Fragment都遵循PageView接口規(guī)范沧奴,這個統(tǒng)一性的要求其實不難,因為一般Android開發(fā)時长窄,都會繼承原生的Activity/Fragment生成項目使用的BaseActivity/BaseFragment滔吠,集中控制已經(jīng)提前做好了,PageView只需在這里實現(xiàn)即可挠日。
8. 網(wǎng)絡(luò)概念層
網(wǎng)絡(luò)通信是一個APP的基礎(chǔ)需求疮绷,除去少數(shù)特例,實際項目開發(fā)都會使用成熟的第三方網(wǎng)絡(luò)庫來著構(gòu)建自己的網(wǎng)絡(luò)功能實現(xiàn)嚣潜,我們項目中使用的網(wǎng)絡(luò)庫是Volley冬骚,人人車App對于網(wǎng)絡(luò)性能指標沒有非常的要求,功能夠用即可懂算。
在重構(gòu)網(wǎng)絡(luò)層之前只冻,項目對Volley做的是相對簡單的功能層封裝,即提供一個函數(shù)可以根據(jù)給出的請求相關(guān)參數(shù)構(gòu)造一個Volley Request计技,并投遞給Volley庫來發(fā)起請求喜德,這個做法在項目初期并沒有什么太大的問題。
但是隨著功能需求的演進垮媒,除了單純的網(wǎng)絡(luò)通信功能外舍悯,項目還增加了很多作用于網(wǎng)絡(luò)請求本身的需求,比如睡雇,對網(wǎng)絡(luò)請求或者回復(fù)的附加處理(Request Processor)萌衬,網(wǎng)絡(luò)認證模塊(Authenticate)會要求攔截認證失敗的請求并自動重發(fā)等,隨著這些需求的疊加它抱,項目代碼和Volley產(chǎn)生了緊耦合秕豫,因為我們需要基于Volley提供的各種類或函數(shù)來發(fā)起請求和實現(xiàn)其他附加網(wǎng)絡(luò)業(yè)務(wù)需求。
上圖上半部是重構(gòu)前的網(wǎng)絡(luò)層架構(gòu)观蓄,可以看到混移,一些業(yè)務(wù)需求的實現(xiàn)邏輯已經(jīng)部分或者全部的位于了Volley的領(lǐng)域。這個情況初看是不可避免的蜘腌,因為你要使用一個庫沫屡,必然要使用其提供的類或者函數(shù)。但是撮珠,對網(wǎng)絡(luò)通信這種底層服務(wù)機制來說沮脖,現(xiàn)在的情形是底層具體實現(xiàn)(Volley)綁架了上層(項目代碼)金矛,上層需要根據(jù)Volley的類和功能來實現(xiàn)網(wǎng)絡(luò)請求處理邏輯,兩者之間正確的關(guān)系應(yīng)該是底層遵循依賴上層提供的需求接口來適配自己的功能勺届。
從一個更抽象的維度看驶俊,項目邏輯需要一個“概念”上的網(wǎng)絡(luò)層,這個”概念”網(wǎng)絡(luò)層的接口和結(jié)構(gòu)均由上層按照自己對網(wǎng)絡(luò)通信的需求和理解制定免姿,下層的具體實現(xiàn)反過來則需要遵循或者適配這套上層“協(xié)議”饼酿。
基于上述思路,引入網(wǎng)絡(luò)概念層胚膊,新的網(wǎng)絡(luò)層架構(gòu)如上圖下半部分所示: Custom Request是上層對網(wǎng)絡(luò)請求這一概念的封裝和落地故俐,成為項目代碼中網(wǎng)絡(luò)請求的唯一表現(xiàn)形式和載體,第三方庫提供的具體請求類型(有的庫甚至連請求類都沒有)被隔離到最底層紊婉,只在Sender(負責(zé)對接第三方庫的適配者)的適配實現(xiàn)中可見药版。Custom Request成為整體架構(gòu)中的一個對外網(wǎng)關(guān)協(xié)議,所有的網(wǎng)絡(luò)請求在內(nèi)部均以Custom Request的形式創(chuàng)建和維護喻犁,最后由當(dāng)前的Sender實現(xiàn)來翻譯/適配為對應(yīng)網(wǎng)絡(luò)庫的網(wǎng)絡(luò)請求對象槽片。
Custom Request采取注入式的方式和第三方庫的具體請求對象協(xié)作,本身只提供對請求相關(guān)信息的查詢(比如請求的地址肢础,參數(shù)等)和回調(diào)接口还栓,不會維持對第三方具體實現(xiàn)的引用,也感知不到第三方庫传轰。第三方網(wǎng)絡(luò)請求會維護一個到通用Request的單向引用剩盒,以擴展的方式來回調(diào)驅(qū)動Custom Request,然后Custom Request進一步的將回調(diào)消息通過Network Callback反饋給上層使用者路召,組成了一條單向底層網(wǎng)絡(luò)回調(diào)反饋鏈條勃刨。Custom Request的主要職責(zé)是網(wǎng)絡(luò)請求信息的載體,基本不包含邏輯股淡,像Authenticate之類網(wǎng)絡(luò)擴展功能的實現(xiàn)會放在Interceptor中。
Interceptor負責(zé)攔截Custom Request的各個回調(diào)點廷区,并將回調(diào)廣播給Interceptor內(nèi)部的所有的Processor唯灵,每個附加功能對應(yīng)一個Processor。Processor在合適的回調(diào)點對請求和回復(fù)進行攔截處理來實現(xiàn)自己負責(zé)的功能隙轻,同以前基本所有的處理邏輯直接混雜在Volley Request內(nèi)部相比埠帕,實現(xiàn)了職責(zé)分離。更關(guān)鍵的是: 所有網(wǎng)絡(luò)附加處理邏輯現(xiàn)在都作用于Custom Request上玖绿,而不是Volley Request上敛瓷,處理邏輯徹底和Volley劃清了界限。
網(wǎng)絡(luò)概念層的另一個收益是加強了對網(wǎng)絡(luò)的控制斑匪,因為Custom Request處于項目領(lǐng)域內(nèi)呐籽,每個發(fā)出的Custom Request完全可以被項目代碼進行維護和管理(如果還使用Volley Request的話,第三方庫是黑盒實現(xiàn),很難說外部維護一個Volley Request沒有潛在風(fēng)險狡蝶,這還會導(dǎo)致之前所述的業(yè)務(wù)邏輯和Volley產(chǎn)生了耦合)庶橱,對網(wǎng)絡(luò)請求狀態(tài)就有一個大局觀,比如當(dāng)前有哪些請求正在進行中以及處于何種狀態(tài)等贪惹。
網(wǎng)絡(luò)概念層對第三方網(wǎng)絡(luò)庫也有功能延展性苏章,畢竟是作為中間層存在。比如網(wǎng)絡(luò)庫本身不支持取消請求的話奏瞬,網(wǎng)絡(luò)概念層完全可以擴展Custom Request來增加一個canceled標記枫绅,上層想取消請求時設(shè)置此標記,盡管底層網(wǎng)絡(luò)庫的請求不能被取消硼端,最終還是會回調(diào)到Custom Request并淋,但是Custom Request完全可以檢查canceled標記來將此回調(diào)忽略不向上傳遞,那么在上層來看显蝌,這個請求就是被成功取消的预伺,更多的擴展應(yīng)用限于篇幅,不再敘述曼尊。
總結(jié)來說酬诀,我們自己制定了一套網(wǎng)絡(luò)層規(guī)范和流程,內(nèi)部以這套準則來操作和管理網(wǎng)絡(luò)請求骆撇,外部(第三方網(wǎng)絡(luò)庫)需要通過適配來提供底層運作瞒御。
9. 功能模塊進化為功能服務(wù)
在功能模塊化推進到一定程度后,開發(fā)效率確實得到了提升神郊,新頁面需要添加以前的功能時直接使用功能模塊即可肴裙。不過在使用中還存在一些痛點:
使用者需要自己創(chuàng)建和維護功能模塊,并將所在的Activity/Fragment生命周期變化傳遞給模塊涌乳。
某些模塊在創(chuàng)建時還需要提供Activity/Fragment作為基點蜻懦,這樣就限制了模塊使用的靈活性,在Activity本身承載的業(yè)務(wù)被分拆為不同的業(yè)務(wù)視圖模塊后夕晓,業(yè)務(wù)視圖模塊如果想要使用功能模塊宛乃,必須也能提供基點,這為業(yè)務(wù)視圖模塊增加了額外的需求蒸辆。
因此考慮將功能模塊升級為功能服務(wù)來緩解上述痛點征炼,這里對功能服務(wù)的定義是: 不需要使用者顯式的去創(chuàng)建服務(wù)以及維護服務(wù)狀態(tài),只需要單純的使用其提供的功能即可躬贡。就像一個靜態(tài)函數(shù)一樣谆奥。
上面描述的功能服務(wù)將使用功能模塊的額外開銷封裝在自己內(nèi)部,使用者的職責(zé)得到簡化拂玻。借助于之前介紹的錨點系統(tǒng)酸些,這個構(gòu)想是在一定程度上可以實現(xiàn):
基于這樣一個前提: 絕大多數(shù)情況下宰译,功能的發(fā)起都是由用戶在界面上觸發(fā)的,比如砍價擂仍,預(yù)約等囤屹。這意味著,在功能被觸發(fā)時逢渔,其所在的Activity/Fragment就是錨點系統(tǒng)維護的前臺PageView肋坚,那么,功能性模塊所需要的基點通過查詢錨點系統(tǒng)即可輕松獲得(錨點系統(tǒng)是全局服務(wù)肃廓,可以在任何地方調(diào)用)智厌,功能模塊創(chuàng)建需要的基點獲取限制就不存在了。
功能模塊的創(chuàng)建和維護封裝在功能服務(wù)內(nèi)部盲赊,功能模塊的創(chuàng)建則基于這樣的現(xiàn)狀: 一個功能即使在一個頁面模塊(Activity/Fragment)上有多個觸發(fā)點铣鹏,功能模塊實例也只需要一個,功能模塊和頁面是一對一的哀蘑。不過因為功能服務(wù)會同時為復(fù)數(shù)個頁面提供服務(wù)诚卸,那么,在功能服務(wù)內(nèi)部就需要同樣數(shù)量的功能模塊绘迁,功能模塊的索引正好通過頁面的PageId實現(xiàn)合溺。另外為了讓功能模塊可以感知生命周期變化,功能服務(wù)還通過錨點系統(tǒng)監(jiān)聽了所有頁面的生命周期狀態(tài)缀台,在收到頁面狀態(tài)變化時使用頁面PageId找到對應(yīng)的功能模塊實例棠赛,繼而將變化傳遞給功能模塊。
至此膛腐,使用的功能模塊所有額外操作都被功能服務(wù)包辦了睛约。外界使用功能服務(wù)變得極其快捷,只需要獲得功能服務(wù)的全局實例哲身,調(diào)用服務(wù)接口即可辩涝。功能服務(wù)對功能模塊進行了包裝,功能實現(xiàn)的主體依然在功能模塊中勘天,生命周期等管理則放在了功能服務(wù)包裝層中膀值。
10. 全局網(wǎng)絡(luò)響應(yīng)處理機制
全局網(wǎng)絡(luò)響應(yīng)處理機制的其實是網(wǎng)絡(luò)層Interceptor中的一個Processor,在這里專門列出來是因為它是框架之間良性協(xié)作的成果误辑,兩套單獨的機制基于不同的目的被開發(fā)出來,兩兩之間產(chǎn)生規(guī)母璺辏化效應(yīng)巾钉,衍生出新的機制或者演化方向。全局網(wǎng)絡(luò)響應(yīng)處理機制是網(wǎng)絡(luò)概念層和錨點系統(tǒng)配合的一個案例秘案。
全局網(wǎng)絡(luò)響應(yīng)處理機制現(xiàn)在只內(nèi)置了一個功能: 全局的驗證碼彈層砰苍。驗證碼彈層是人人車App的基礎(chǔ)功能潦匈,所有的線索提交均有可能收到特定的回復(fù),要求輸入驗證碼后再重新提交赚导,并且支持驗證碼的刷新(也需要重新發(fā)送請求)和驗證錯誤提示茬缩。彈層使用Android的Dialog實現(xiàn)。
驗證碼彈層的特殊之處在于: 用戶提交請求后得到服務(wù)器回復(fù)要求輸入驗證碼吼旧,是一個異步網(wǎng)絡(luò)過程凰锡,但是Dialog需要Activity作基點。 在最初的實現(xiàn)里圈暗,需要在回調(diào)對象中保存Activity的弱引用掂为,回調(diào)對象中還包含了對驗證碼各種分支邏輯的處理,這種方式有幾個不足之處:
弱引用不夠優(yōu)雅以及限制了使用場景(你需要一個能獲得Activity的場景)员串。
在每個可能觸發(fā)驗證碼的請求發(fā)起點勇哗,都要顯式的使用這個驗證碼處理回調(diào)類,遇到一些對響應(yīng)有特殊處理的情況寸齐,還必須繼承這個回調(diào)類來保證驗證碼和特殊處理邏輯的兼顧欲诺。
上述缺陷從職責(zé)層面上講,是提交請求者承擔(dān)了驗證碼的處理職責(zé)渺鹦。但是驗證碼機制和具體的請求響應(yīng)處理之間是不應(yīng)該有什么關(guān)聯(lián)的扰法,驗證碼是請求響應(yīng)的一個全局前置步驟,在下游具體的請求響應(yīng)處理不應(yīng)該感知到驗證碼海铆。
理想的驗證碼處理應(yīng)該為下游的請求回復(fù)處理屏蔽掉驗證碼的存在迹恐,下游可以認為這是一個不需要驗證碼的世界。要實現(xiàn)這個效果卧斟,必然要對所有網(wǎng)絡(luò)回復(fù)進行前置攔截殴边,網(wǎng)絡(luò)層 Interceptor正好提供這樣一個切面,驗證碼機制可以作為其中的一個Processor存在珍语。這樣驗證碼機制在架構(gòu)中的位置和層級就確定了锤岸。
如前所述,驗證碼機制一旦發(fā)現(xiàn)驗證碼相關(guān)回復(fù)就予以攔截板乙,根據(jù)回復(fù)內(nèi)容展示或者刷新驗證碼彈層是偷,這就回到上面描述的基點獲取問題,驗證碼彈層展現(xiàn)需要的Activity怎么獲得募逞? 驗證碼機制在Interceptor這一層只能看到網(wǎng)絡(luò)請求和網(wǎng)絡(luò)回復(fù)蛋铆,得不到發(fā)起請求時的Activity。 簡單的想放接,就會試圖把Activity保存在網(wǎng)絡(luò)請求對像中刺啦,這是一個糟糕的做法,導(dǎo)致內(nèi)存泄露纠脾,也破壞了網(wǎng)絡(luò)請求對象的結(jié)構(gòu)玛瘸,在一個網(wǎng)絡(luò)請求對象中維護一個Activity是比較違和的蜕青,兩者在層次上不匹配。我們需要一種更優(yōu)雅輕量的方式來讓驗證碼機制可以獲取Activity糊渊。使用錨點系統(tǒng)提供提供的PageId就是個不錯的輕量級方案右核,PageId保存在請求對象中并不顯得突兀,因為它和請求對象處于同一個抽象層次渺绒。
驗證碼機制要滿足同時處理復(fù)數(shù)個網(wǎng)絡(luò)請求的場景贺喝,而這復(fù)數(shù)個網(wǎng)絡(luò)請求又可能來自不同的頁面,所以驗證碼的處理要先以頁面為維度進行區(qū)分芒篷,再以網(wǎng)絡(luò)請求為維度進行細分(得益于網(wǎng)絡(luò)概念層對網(wǎng)絡(luò)請求的封裝搜变,區(qū)分網(wǎng)絡(luò)請求可以使用Custom Request的RequestId)稻薇,網(wǎng)絡(luò)回復(fù)會被驗證碼機制分發(fā)到對應(yīng)的發(fā)起頁面的網(wǎng)絡(luò)回復(fù)處理接口中(借助于網(wǎng)絡(luò)請求攜帶的PageId和錨點系統(tǒng)狡恬,我們可以得到合適的頁面實體驮瞧,當(dāng)然了狗唉,對于對應(yīng)頁面已經(jīng)被銷毀的網(wǎng)絡(luò)請求漆际,就沒有進一步處理的必要屑迂,直接放行任其消逝即可)拇颅。
頁面隨后承擔(dān)起驗證碼處理的后續(xù)邏輯荧恍,因為每個請求都需要自己的驗證碼彈層镰烧,因此需要為每個請求單獨維護一個驗證碼彈層的Handler拢军,Handler承接了驗證碼彈層的顯示和驗證重發(fā)(驗證重發(fā)得益于對網(wǎng)絡(luò)請求的建模,非常簡單怔鳖,并且重發(fā)不會改變RequestId茉唉,這樣使得重發(fā)后的回復(fù)可以繼續(xù)被相應(yīng)的處理器處理)等邏輯, 可以使用請求的RequestId作為key組織Handler映射表來管理復(fù)數(shù)個請求的Handler结执。請求成功或者失敗都代表著請求的終結(jié)度陆,此時可以釋放其Handler。
只有非驗證碼相關(guān)回復(fù)才能被驗證碼機制放行献幔,使得驗證碼機制對下游的回復(fù)處理邏輯是透明的懂傀。這樣,借助于復(fù)數(shù)個已有機制的特性和少許的擴展蜡感,我們將原來的分散零碎的驗證碼處理邏輯聚合上升成為一個獨立透明的中間層蹬蚁。
第二階段演進總結(jié)
錨點系統(tǒng)提供了輕量頁面索引和頁面全局信息查詢功能,解放了一部分原先顯式依賴Activity/Fragment的實現(xiàn)郑兴,也為某些依賴接頁面狀態(tài)的需求提供了更快捷的功能接口犀斋。
網(wǎng)絡(luò)概念層的建立將項目代碼和第三方網(wǎng)絡(luò)庫解耦,所有的上層機制都基于項目本身對網(wǎng)絡(luò)的概括理解來實現(xiàn)情连,同時還獲得了對網(wǎng)絡(luò)請求全局信息的掌控闪水。
功能服務(wù)進一步分化了職責(zé),功能使用開銷得到進一步壓縮。
全局網(wǎng)絡(luò)響應(yīng)處理機制將散落在各處的驗證碼處理邏輯集中為一個對下游透明的中間層球榆。
總結(jié): 第二階段的重構(gòu)偏向于框架化和服務(wù)化,通過引入全局性的機制在新的維度實現(xiàn)需求和改良架構(gòu)禁筏,機制之間的良性協(xié)作效應(yīng)開始顯現(xiàn)持钉。
結(jié)語
人人車Android客戶端開發(fā)周期已近兩年,迭代30多個版本篱昔,歷經(jīng)數(shù)次規(guī)模不等的重構(gòu)每强,篳路藍縷,終有小成州刽。筆者有幸參與并主導(dǎo)了App的從萌芽到成長空执。盡管受限于技術(shù)水平和經(jīng)驗視野,我們的架構(gòu)演進并沒有實現(xiàn)最優(yōu)解穗椅。但于我辨绊,這是一次偉大的朝圣之旅。