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