京东支付SDK重构设计与实现
背景
眾所周知,軟件開發(fā)效率、維護(hù)成本與自身復(fù)雜度成正比,而客戶端軟件復(fù)雜度則主要體現(xiàn)在業(yè)務(wù)規(guī)模上。
京東支付Android SDK從2015年啟動以來,已歷經(jīng)五個(gè)春秋,如今發(fā)展到純支付業(yè)務(wù)代碼7.5W行的規(guī)模(不含支付團(tuán)隊(duì)內(nèi)部基礎(chǔ)組件庫和兄弟團(tuán)隊(duì)生物識別、安全等近10個(gè)SDK)。為應(yīng)對每年618、11.11大促考驗(yàn),內(nèi)置各種降級邏輯致使部分功能要準(zhǔn)備至少兩種技術(shù)實(shí)現(xiàn)方案,復(fù)雜度不言而喻。雖然久經(jīng)沙場,然而步履愈發(fā)沉重。究其原因,無外乎技術(shù)圈這些司空見慣的槽點(diǎn):
業(yè)務(wù)發(fā)展太快,早期技術(shù)架構(gòu)已經(jīng)不能很好的適應(yīng)變化,而業(yè)務(wù)需求又繁重,架構(gòu)升級計(jì)劃一次次被延后,最后不了了之。
既然架構(gòu)不能支持新業(yè)務(wù),就只能通過各種“旁門左道”的方式破壞架構(gòu)來解決問題,以至于進(jìn)化成沒有架構(gòu),只有各位前輩高人饋遺的祖?zhèn)魈茁?#xff0c;謂之“祖宗家法不可變”。
沒有實(shí)際價(jià)值的業(yè)務(wù)代碼一直茍延殘喘的留在系統(tǒng)里,變成長期的維護(hù)負(fù)擔(dān)。
設(shè)計(jì)文檔、接口文檔、代碼注釋缺失或更新不及時(shí),致使涉及多系統(tǒng)交互的代碼后人往往只能因循將就,不敢輕言優(yōu)化。
有鑒于此,為使京東支付SDK未來能輕快地奔跑,從容應(yīng)對變化,我們決定重構(gòu)。目標(biāo):實(shí)現(xiàn)軟件復(fù)雜度增長低于業(yè)務(wù)復(fù)雜度增長的目標(biāo)。
一、支付業(yè)務(wù)組成
常言道:脫離業(yè)務(wù)的架構(gòu)都屬于自嗨。
為實(shí)現(xiàn)重構(gòu)目標(biāo),我們需要:
先梳理清楚業(yè)務(wù)特點(diǎn),做業(yè)務(wù)層抽象;
找出當(dāng)前軟件系統(tǒng)痛點(diǎn)所在,做技術(shù)層分析;
結(jié)合業(yè)務(wù)層抽象與技術(shù)層分析,設(shè)計(jì)新解決方案;
上圖比較宏觀的把SDK劃分為幾大組成單元,特點(diǎn)是:
所有組成單元之間都是雙向依賴,任何一個(gè)業(yè)務(wù)單元都可以作為其他業(yè)務(wù)單元的前置流程,也可以成為其他業(yè)務(wù)單元的下一步流程,很多業(yè)務(wù)單元內(nèi)部還存在互相依賴。而這種循環(huán)、交叉的依賴,重構(gòu)之難可想而知,修改一處影響一片。每當(dāng)試圖把重構(gòu)拆分成多個(gè)小任務(wù)來迭代執(zhí)行時(shí)就會發(fā)現(xiàn),粒度實(shí)難控制,因?yàn)楦闹闹蜕婕吧习賯€(gè)文件了…
業(yè)務(wù)變種眾多,舉個(gè)例子,僅短信驗(yàn)證一個(gè)功能就有內(nèi)單、外單、支付驗(yàn)證、風(fēng)控加驗(yàn)、白條開通、證書安裝、全屏頁、半屏頁、特殊業(yè)務(wù)等諸多變種,這些變種彼此組合才能完成一個(gè)短信驗(yàn)證操作,如“內(nèi)單+風(fēng)控加驗(yàn)+半屏”這幾個(gè)組合就是一種常見的短信驗(yàn)證流程,而“外單+風(fēng)控加驗(yàn)+全屏”又是另一種組合,依此類推。
異常流程繁雜,為了盡可能使用戶完成支付,必須識別并區(qū)別處理各種失敗情況。如:忘記密碼的要引導(dǎo)用戶找回密碼、余額不足的要引導(dǎo)用戶更換支付方式等等。異常流程往往伴隨著多次支付流程重試行為,也就是說已經(jīng)執(zhí)行過的流程,部分?jǐn)?shù)據(jù)要保留,部分?jǐn)?shù)據(jù)要替換,因此,確保模塊重新執(zhí)行時(shí)入?yún)⒑统鰠⒌木珳?zhǔn)性也是一大難題。
二、經(jīng)典架構(gòu)模式能否解決問題?
京東支付SDK一直以來使用的是MVP模式,它的優(yōu)勢在于分離UI與業(yè)務(wù)邏輯,即關(guān)注單個(gè)頁面及相關(guān)數(shù)據(jù)、業(yè)務(wù)代碼如何構(gòu)建。其核心聚焦于“點(diǎn)”上。而對支付業(yè)務(wù)而言,任何一個(gè)單一頁面都算不上復(fù)雜,它的復(fù)雜性體現(xiàn)在如何把這些簡單的頁面(點(diǎn))串聯(lián)起來組成一個(gè)可執(zhí)行的業(yè)務(wù)鏈(線)。同理,MVC、MVVM等經(jīng)典模式同樣也無法解決由點(diǎn)到線的問題。而VIPER模式有人把它比喻為搭樂高,可以串聯(lián)各個(gè)模塊,它里面包含的R(Router)確實(shí)是處理模塊跳轉(zhuǎn)用的,這么看似乎有機(jī)會解決點(diǎn)到線的問題,那么可否一戰(zhàn)呢?我們來進(jìn)一步分析。
這是網(wǎng)上流傳很廣泛的一張圖,View和Presenter無需多說,Router負(fù)責(zé)模塊(頁面)跳轉(zhuǎn),而Entity和Interactor大體上是把傳統(tǒng)的Model職責(zé)拆開,純數(shù)據(jù)對象作為Entity(Bean),Interactor用來管理調(diào)度數(shù)據(jù)。但是,問題在于怎么來管理數(shù)據(jù)?我們考慮有兩種可能:
1、將Interactor設(shè)計(jì)為Presenter級別數(shù)據(jù)管理器
這樣的話,那么支付這種模塊眾多且交叉、循環(huán)耦合的業(yè)務(wù),誰來處理模塊間數(shù)據(jù)流轉(zhuǎn)的準(zhǔn)確性呢?如圖所示,Interactor與Router并沒有直接交互,而是通過Presenter來處理。這就使得單個(gè)模塊的Presener可能需要知道其他模塊所需的數(shù)據(jù)來自哪里,以及如何組裝出下個(gè)模塊的入?yún)?#xff0c;如此一來,Presenter難免感知、耦合其他模塊。當(dāng)一個(gè)模塊耦合了一堆其他模塊之時(shí),牽一發(fā)動全身就不難理解了。不幸的是,京東支付SDK重構(gòu)前就存在這種情況,各種驗(yàn)證工具模塊更是重災(zāi)區(qū),因?yàn)閹缀趺糠N驗(yàn)證工具的Presenter中都包含了一堆業(yè)務(wù)場景的定制邏輯。舉個(gè)例子:
密碼驗(yàn)證Presenter由A、B、C業(yè)務(wù)調(diào)用時(shí)的入?yún)ⅰ⒊鰠⒏鞑幌嗤?#xff0c;下一步流程也不一樣,這種情況下如果Router的數(shù)據(jù)由密碼驗(yàn)證Presenter來提供的話,勢必要耦合前后各種不同的業(yè)務(wù)邏輯。那么,如果給每種業(yè)務(wù)場景提供專屬Presenter怎么樣呢?支付SDK重構(gòu)前也是這么做的,僅短信驗(yàn)證至少就有8種對接不同業(yè)務(wù)的Presenter實(shí)現(xiàn),然而并不能徹底解決問題,因?yàn)槊糠N驗(yàn)證方式都可能銜接N種后續(xù)流程,所以在短信驗(yàn)證Presenter里構(gòu)建Router數(shù)據(jù)還是免不了把其他流程的邏輯亂入進(jìn)來。這也是多年以來一直困擾支付SDK的一大問題:讓一個(gè)模塊只做自己這一件事兒,太難了。
2、將Interactor設(shè)計(jì)為全局?jǐn)?shù)據(jù)管理器
其實(shí)Interactor作為數(shù)據(jù)管理器最重要的功能是調(diào)度數(shù)據(jù),而擁有更高更廣的視角似乎也更有利于完成這項(xiàng)工作。同時(shí),作為全局調(diào)度器,收納并管控各種流程特定數(shù)據(jù)、調(diào)用邏輯,看起來也是理所應(yīng)當(dāng)。因此,我們設(shè)想把所有模塊做成類似系統(tǒng)Widget一樣的組件,暴露出各種原子級別API,自身只負(fù)責(zé)UI渲染和處理內(nèi)部交互,所有涉及外部的交互全部拋出去,使模塊達(dá)到不知自己從哪來,更不知自己上哪去的目標(biāo)(傳說中的高內(nèi)聚、低耦合)。
三、Scene與Interactor,DDD設(shè)計(jì)實(shí)踐
由于支付SDK是單Activity多Fragment設(shè)計(jì),Router本身并沒有太多復(fù)雜性可言,而繁重的邏輯主要集中在數(shù)據(jù)管理和流程調(diào)度中。因此,我們決定把VIPER中I和R的職責(zé)合為一體,再按照DDD設(shè)計(jì)思路將業(yè)務(wù)場景和用戶交互的職責(zé)重新劃分成Scene和Interactor。
- Scene是整個(gè)業(yè)務(wù)流的核心,類似于DDD中領(lǐng)域?qū)?#xff0c;管理并調(diào)度影響主干流程的所有數(shù)據(jù),它與UI無關(guān),但它任何時(shí)候都可以根據(jù)所持有的數(shù)據(jù)知道當(dāng)前業(yè)務(wù)流執(zhí)行到哪一步了,以及下一步要做什么、需要哪些數(shù)據(jù)。
- Interactor是Scene的輔助,與VIPER中Interactor定位不同,它定位為DDD中UI層與應(yīng)用層的結(jié)合,面向業(yè)務(wù)場景(即用例),負(fù)責(zé)處理業(yè)務(wù)流上用戶主動觸發(fā)的關(guān)鍵交互事件(如:頁面之間的跳轉(zhuǎn)、需要其他模塊協(xié)作的),并交由Scene來處理業(yè)務(wù)邏輯,再把結(jié)果反饋給用戶。
如圖所示,Current Business Unit即當(dāng)前正在執(zhí)行任務(wù)的模塊,假設(shè)它是密碼驗(yàn)證模塊,交互如下:
用戶輸入密碼后,該模塊將輸入數(shù)據(jù)封裝成一個(gè)Event事件發(fā)出來;
Interactor識別并接收這個(gè)Event,把它交給Scene中處理密碼輸入的方法進(jìn)行處理;
Scene的密碼處理方法去調(diào)用服務(wù)端接口驗(yàn)證密碼
驗(yàn)證失敗,把錯(cuò)誤信息封裝成Event發(fā)出來,密碼模塊接收并處理;
驗(yàn)證成功,Scene根據(jù)持有的流程數(shù)據(jù)判斷下一步做什么,并將數(shù)據(jù)組裝好,交給Interactor;
Interactor收到Scene處理后的數(shù)據(jù),完成模塊跳轉(zhuǎn)。
這種設(shè)計(jì)的好處在于所有模塊互不相關(guān),響應(yīng)用戶交互的代碼和數(shù)據(jù)也是分離的,業(yè)務(wù)流程全權(quán)由Scene處理,每種業(yè)務(wù)只需開發(fā)自己的Scene和Interactor,即可快速組合已有模塊完成業(yè)務(wù)需求。
四、UserCase
雖然Scene擁有決定業(yè)務(wù)流走向的所有數(shù)據(jù),但面對復(fù)雜業(yè)務(wù)流時(shí),想定位當(dāng)前運(yùn)行到哪一步了,仍然不是件容易的事兒。
簡單而常見的做法是在代碼里加各種狀態(tài)標(biāo)記,但狀態(tài)標(biāo)記過多,尤其還需要組合使用的時(shí)候,就會變成后期沒人敢碰的惡毒機(jī)關(guān)。如:A模塊改變某個(gè)變量值,可能影響到B業(yè)務(wù)的邏輯。眾所周知,數(shù)據(jù)源越分散,代碼邏輯越看清。
考慮到支付業(yè)務(wù)流通常以O(shè)ne By One這種鏈?zhǔn)竭\(yùn)行,倘若我們把業(yè)務(wù)流上每個(gè)業(yè)務(wù)單元當(dāng)成一個(gè)節(jié)點(diǎn),整個(gè)業(yè)務(wù)流當(dāng)成一條鏈,那么,理論上每種業(yè)務(wù)都可以構(gòu)建出一條業(yè)務(wù)鏈,我們把這條鏈定義成一個(gè)UserCase。UserCase上的每一個(gè)業(yè)務(wù)單元按順序執(zhí)行即可完成業(yè)務(wù)流:
new UserCase().business(createBusinessA(), JPPRuntime.getAsyncWorker()).business(createBusinessB(), JPPRuntime.getMainWorkder()).business(createBusinessC(), JPPRuntime.getAsyncWorker()).business(createBusinessD(), JPPRuntime.getMainWorkder()).execute(new Observer() {@Overridepublic void onComplete(@NonNull UserCase userCase) {}@Overridepublic void onError(@NonNull Throwable throwable) {}});與RxJava調(diào)用形式類似,UserCase上每個(gè)業(yè)務(wù)單元都在指定Worker線程運(yùn)行,通常情況下,一個(gè)任務(wù)執(zhí)行完成后會調(diào)用UserCase的next()方法執(zhí)行下一任務(wù)。整個(gè)業(yè)務(wù)流的進(jìn)度是由UserCase來管理的,所以不需要任何數(shù)據(jù)也能知道當(dāng)前正在執(zhí)行哪個(gè)業(yè)務(wù)單元。而UserCase自身又是以雙向鏈表結(jié)構(gòu)存儲各業(yè)務(wù)單元的,也就是說每個(gè)業(yè)務(wù)單元都可以通過UserCase查找到上一個(gè)業(yè)務(wù)單元是誰,下一個(gè)又是誰,這種設(shè)計(jì)的好處在于:
為了使UserCase支持定向跳轉(zhuǎn)和流程回溯,每個(gè)業(yè)務(wù)單元被設(shè)計(jì)為擁有ID(UserCase內(nèi)唯一)和入?yún)ⅰ⒊鰠?#xff08;Input/Output)的組成形式:
public interface Business<I, O> {int getId();I getInput();void setInput(@Nullable I input);O getOutput();void onExecute(@NonNull UserCase userCase, @Nullable Business prev); }- 定向跳轉(zhuǎn)時(shí)UserCase通過ID在業(yè)務(wù)鏈上查找業(yè)務(wù)單元。
- 業(yè)務(wù)單元執(zhí)行的入?yún)?#xff08;Input)由外部傳入,所以允許set,而執(zhí)行后的出參(Output)則是只讀的,這樣每次業(yè)務(wù)單元執(zhí)行后的入?yún)ⅰ⒊鰠⒕涂梢孕纬梢环輸?shù)據(jù)快照,UserCase回溯流程時(shí)便有跡可循。為保證每個(gè)業(yè)務(wù)單元數(shù)據(jù)快照的穩(wěn)定性,避免引用型入?yún)ⅰ⒊鰠⒈煌獠啃薷牡膯栴},我們還開發(fā)了一個(gè)數(shù)據(jù)深拷貝工具,實(shí)現(xiàn)一行代碼復(fù)制任何對象(包括對象內(nèi)所有層級的子對象)。
五、業(yè)務(wù)模版
重構(gòu)以后,支付SDK每個(gè)業(yè)務(wù)場景都有一個(gè)特定的Scene、Interactor和眾多業(yè)務(wù)單元,如圖:
-
每個(gè)BusinessUnit都實(shí)現(xiàn)了Business接口,其中內(nèi)聚了該業(yè)務(wù)相關(guān)的入?yún)ⅰ⒊鰠⒑虸D;
-
BusinessScene和BusinessInteractor是配對關(guān)系,彼此互相引用緊密協(xié)作;
-
BusinessScene集成了特定業(yè)務(wù)場景所需的所有BusinessUnit(如:密碼驗(yàn)證、收銀臺、綁卡等模塊);
-
BusinessInteractor在createUserCase()時(shí),從BusinessScene中獲取這些BusinessUnit并編排業(yè)務(wù)鏈,生成該業(yè)務(wù)的UserCase;
-
onEvent()接收并處理各BusinessUnit與用戶交互過程中需要BusinessScene/BusinessInteractor配合的事件,如:需要驗(yàn)證密碼時(shí),當(dāng)前BusinessUnit發(fā)出請求驗(yàn)證密碼事件,BusinessInteractor接收到以后請求BusinessScene根據(jù)當(dāng)前流程狀態(tài)決定展示何種密碼驗(yàn)證頁,BusinessScene把結(jié)果(密碼驗(yàn)證頁入?yún)?#xff09;告知BusinessInteractor,并由BusinessInteractor啟動密碼驗(yàn)證頁;
六、京東支付SDK新架構(gòu)
如前文所述,此次重構(gòu)專注于重組SDK業(yè)務(wù)邏輯,使新架構(gòu)能更好的支持業(yè)務(wù)需求迭代,提升開發(fā)效率。總結(jié)起來如下:
首先,根據(jù)業(yè)務(wù)流來重新組織代碼,每個(gè)業(yè)務(wù)流就是一套Scene+Interactor+UserCase的組合,可以理解為一個(gè)業(yè)務(wù)沙箱,沙箱內(nèi)是完整的業(yè)務(wù)運(yùn)行時(shí)環(huán)境,不支持的功能,不會存在于沙箱中,也就不會在運(yùn)行時(shí)意外亂入,而整個(gè)業(yè)務(wù)流由Scene+Interactor+UserCase組合來決策;
其次,業(yè)務(wù)單元Widget化,只做自己本職工作,絕不插手業(yè)務(wù)流程;
再次,充分利用事件驅(qū)動模型來解耦業(yè)務(wù)單元間的依賴關(guān)系,承擔(dān)全局消息總線職責(zé);
最后,為了滿足宿主App對SDK功能、體積的要求,重構(gòu)后把非標(biāo)業(yè)務(wù)或功能做了成動態(tài)模塊,通過Gradle在編譯時(shí)一鍵配置是否集成進(jìn)SDK中。動態(tài)模塊另外一個(gè)好處是,可以支持定制化需求,又不必深度入侵標(biāo)準(zhǔn)業(yè)務(wù)。
七、重構(gòu)收益
我們以同一版本京東App為宿主,分別把新、老兩個(gè)SDK集成進(jìn)去,在相同入口用相同訂單測試:
1、啟動時(shí)長對比
啟動時(shí)長指:從京東支付SDK主Activity啟動到第一個(gè)接收用戶交互的Fragment響應(yīng)onResume()生命周期這段時(shí)間,其間包含了一次后端接口調(diào)動,但多次測試使用的參數(shù)是一樣的。
| 第一次時(shí)長(ms) | 6619 | 3549 |
| 第二次時(shí)長(ms) | 7809 | 4265 |
| 平均時(shí)長(ms) | 7214 | 3907 |
2、純業(yè)務(wù)(Java)代碼量對比
| 代碼總行數(shù) | 75778 | 35820 |
| 文件個(gè)數(shù) | 604 | 355 |
| 總大小(kB) | 3574 | 1686 |
| 單個(gè)最大(kB) | 155 | 101 |
3、資源文件(XML)對比
| 代碼總行數(shù) | 14204 | 7688 |
| 文件個(gè)數(shù) | 238 | 143 |
| 總大小(kB) | 681 | 398 |
| 單個(gè)最大(kB) | 38 | 30 |
關(guān)于重構(gòu),我們總是不好量化收益,因?yàn)榇a是否更易于維護(hù),無法量化,用戶也感受不到。但是我們可以很容易理解的是:代碼量大幅縮減,運(yùn)行時(shí)執(zhí)行的代碼就變少了,性能理所當(dāng)然會提升。
本文作者:京東科技 王超
更多技術(shù)最佳實(shí)踐&創(chuàng)新成果,請關(guān)注“京東數(shù)科技術(shù)說”微信公眾號
總結(jié)
以上是生活随笔為你收集整理的京东支付SDK重构设计与实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 三元运算符 在数据绑定中的使用
- 下一篇: 词汇挖掘与实体识别(未完)