京東app後台多端融合架構代碼重構實戰

語言: CN / TW / HK

一 簡介

重構是一個非常常見且古老的課題,涉及重構的文章、書更是不可勝數。

但其實做程序做久了就會知道,想把一個複雜的系統做好,尤其是參與人數較多的中大型項目,靠看幾本設計模式的書,去試圖尋找設計模式的奧祕,其實是不夠的。很多時候,看書時覺得很有道理,例子也能理解,但到實際開發時,卻無從下手,不知道怎麼靈活套用。

很多項目,在持續的版本迭代中,還伴隨着人員的更替過程,往往為了解決眼前的需求,最常見的就是直接複製類似的邏輯,或者就是在末尾追加邏輯。同時,受限於對老版本的需求理解,很容易出現新需求覆蓋老需求的參數值,無意識的更改老版本結果等。那麼倘若系統的隔離性做的不好,則極易產生A功能的改動,影響BCD等一大堆功能的正確性。

以京東App的後台為例,就是一個典型的複雜系統,涉及開發人員眾多,模塊巨多,迭代時間很長,很多業務邏輯已無從考證,開發人員也已經換了好幾輪,那麼對於這樣的系統,如何讓開發人員做的需求、功能隔離開,互不影響,各小模塊又能各自健壯、系統又具備相當的擴展性、配置化率又高(僅通過配置即可完成功能的變更),就成了一個不得不深入考慮的問題。

本篇主要是我在實際工作中,對一個複雜系統做了一些改造,並從中總結的一些經驗,做的分享。

二 案例

近期京東App後台核心模塊發生了較大的邏輯改動,主要原因是新增了一些接入來源,從之前的獨苗京東App,到後來的京東PC站、京東極速版、老年版、小程序等等,都接入了原App後台。通過完成統一接入,避免了多個後台共存,重複開發的問題。

而這些不同的來源,邏輯就有較多不同。

有的來源需要執行邏輯ABC,有的來源需要執行BCD,有的只需要執行BC,並且不同的來源返回值也有所不同,這就對之前的單一來源的系統架構產生了較大的衝擊,如果處理不當,則不可避免地出現大量的if-else邏輯,以及擴展上的混亂。

那麼針對這種情況,以及對提高系統整體配置化率的訴求,我們對後台架構做了一次重構。本文就是對重構內容做的一個濃縮後的抽象講解,線上實戰性質,非單純設計模式類的demo。

如下圖,我儘可能簡化了細節,和小模塊內的邏輯,僅保留了最外層的大模塊。

我們來看背景,之前只有主App客户端來源的請求,譬如refer=1,該請求到達後,需要觸發"運費"、"優惠券"等數十個上游rpc調用,之後聚合各上游系統結果,返回給客户端對應結果。

現在新增的接入方,譬如"老年版",refer=5,就刪減了組件層一些複雜的促銷邏輯、湊單、白條之類,結果層也有相應刪改。我們該如何支撐這種可能隨時增刪改模塊和來源的業務架構呢?

 

三 原始問題點——複雜的排列組合

當多個層級均出現了多個變量時,這個系統的邏輯就變成了一個複雜的排列組合問題。

我們假如用户的入參是User對象,裏面有一個字段refer表明了來源。

在重構前,代碼進入主流程後,如下圖,就是簡單地根據refer來決定是否走哪些模塊、返回哪些參數。

圖中僅作為示例,實際情況,每個 fetch模塊都有數千數萬行代碼,邏輯之複雜,各種if、else運用之嵌套,各種與或非使用之犀利,實屬魯班再世,也要誇讚幾句鬼斧神工的。

大家都能看出來的問題,就是業務模塊與入參變量的強耦合,如果入參refer=1,則執行業務模塊A、B、C,不執行E、F、G,且返回值包含X、Y、Z,不包含U、V、W。

這樣的設計不可避免帶來了極大的維護的麻煩與混亂,到處都在判斷是否是它,是他還是她?

那麼該如何隔離層級,解耦模塊與來源、來源與返回值之間的關聯呢。

四 簡化判斷,讓職責單一

上面提到了最大的問題,我們用通俗的話來講就是:如果是A,我就做A1、A2、A3;如果是B,我就做A2,A3,A4。

那麼問題就是這個主邏輯器做了太多的事情,日後被修改的概率極大,每個邏輯變動,都會導致主邏輯器的改動。

我們主要優化的點就是將這個邏輯給去掉,讓主邏輯器職責單一,每個業務單元也職責單一。將上面的邏輯變成如果是A1,則來源是A時我工作,是B時我不工作,如果是A2,則來源A、B我都工作,如果是A4,則來源A我不工作,來源B我工作。

可以看到,做的事情就是當有一堆條件判斷,要決定執行N個邏輯中的M個時,調用者不應該關心調用邏輯,而應由這N個邏輯自行判斷自己要不要執行。

從代碼實現來看,就是調用者不關心有多少個邏輯塊,也不必關心日後的增減,從而實現調用的解耦。那麼代碼該如何寫呢?

原來的是在一個方法裏,fetchStock,fetchDiscount等等,首先我們要把這些實現全部去除,並統一為對接口的遍歷。

改造後的代碼是這樣

代碼很簡單,注入一個接口的集合,並遍歷這個集合,根據實現類返回的true、false決定是否要執行這個實現類的業務邏輯。

接口定義如下

單個邏輯單元代碼如下

以上主流程的邏輯很清晰,後續隨着各模塊的擴展或縮減,都不需要動主邏輯,而只需要各個子模塊根據自己的情況返回是否要執行自己即可。

五 動態配置,避免硬編碼

通過以上的改造,我們已經完成了模塊間的隔離,當有新增、刪減模塊時,可以做到不影響主流程,且將代碼修改、影響範圍控制在一個類裏。

但是需求的變化總是很頻繁,僅僅做到互不影響還不滿足需求,我們還需要做到能夠動態的控制各個模塊的啟用和關閉。

譬如『如果是A,我就做A1、A2、A3;如果是B,我就做A2,A3,A4』。希望能做到隨時僅通過修改配置,不改代碼不重新發布而做到『如果是A,我就做A1、A2;如果是B,我就做A3,A4』,完成對模塊的啟停。

動態配置該如何實現呢?

其實很簡單,我們只需要修改execute方法,將refer==1這種規則存放於配置中心,將execute方法裏的硬編碼判斷變成根據配置中心的配置進行判斷即可。如下圖:

那麼ConfigCenter就是配置中心工具類,裏面提供了根據key獲取value的方法。配置中心大抵如註釋所描述的,在應用啟動時,全量從zk、etcd等拉取配置並保存在本地內存,並開啟監聽,當配置中心內容有變化時,更新到本地內存裏。

通過觀察各個模塊的execute類,可以進一步發現,倘若配置中心裏我們將類名作為key,模塊所支持的refer集合為value時,各模塊的execute方法就是完全一樣的代碼。那麼整個方法又可以進一步抽成一個抽象類,由該抽象類來完成這個判斷邏輯,如圖:

當有了統一完成"開關"的父類後,則各個模塊的邏輯單元就更加簡單了,只需要關注自己的業務邏輯即可。

至此,最小業務單元職責則迴歸到純粹的業務邏輯,不再參與流程控制的邏輯判斷。同理,主流程也不再參與對各個子業務單元的判斷和控制,只關注於對接口的遍歷,各模塊也不再產生相互影響。

六 結果層字段隔離控制

以上我們完成了業務邏輯單元的隔離,那麼對於結果層該如果控制呢?

原始代碼是這樣的,在主流程中對各個變量進行判斷,然後設置結果的值。從原始代碼可以看到,即便只有一個變量refer就已經讓代碼可維護性變的很差,更別提真實場景下變量可能有多個時,要維護不同變量場景下返回不同的參數該多少困難。

通過對上面業務層控制的實現,我們同樣可以採用類似的方式來處理結果層。

定義一個接口如下,定義boolean型方法,讓各字段決定自己要不要返回。定義key、value,用來存放字段名和value。

修改主流程如下,當需要返回時,才能待返回字段的key、value存起來。

實現類如下:

以上方式展示了對結果層進行精細化控制的簡單方案,實際場景中,可能涉及結果層數據結構並不是單層、對key、value的判斷需要額外的屬性等,其實思路都是一樣的。

如果要增加入參的判斷,在接口的needOut裏追加要參加邏輯判斷的入參即可。如果返回的結構不是單層的key-value,則在複合結構的實現類裏再嵌套一層新定義的接口的遍歷也可。

七 長邏輯相關處理

長邏輯這種最常見,也是最好處理的。我們經常在寫一處邏輯時,剛開始很簡單,幾行就解決了,後來隨着業務越來越膨脹,這個方法也是越來越長。終於有一天,代碼長到顯示器裝不下它了,後面的邏輯開始出現對前面的邏輯產生影響了,這個方法就開始變的有"壞味道"。

這種相信大家都不少見,尤其在老系統中,從1千到8千行的我都見過,編輯器右邊的滾動條都要看不到了。當然僅僅是長倒還好説,主要的問題是相互影響,前面賦的值,後面就被覆蓋了,這種問題往往還比較隱蔽,極其影響系統的健康。

解決這種長邏輯,其實很簡單,做好兩件事即可,1-將方法的順序執行變成接口遍歷,2-封裝。

1 如何將方法的順序執行變成遍歷呢?

這個其實在上面已經講了,是類似的做法。將一大堆在同一個大方法裏的小方法全部變成某個接口的實現類,從而將方法的順序執行,變成對接口集合的遍歷執行。後續增加或刪減方法時,只操作對應的類即可,而不需要對這個大流程做修改。

一個單獨的實現類就是這樣的:

 

可能有人會問,我的各個方法是有先後順序的,你用了接口集合,該怎麼控制順序呢,從上圖的Order註解可以看到,這個就是控制在接口實現類的順序的,值越小,在List裏越靠前。

2 如何理解封裝呢?

這個更簡單,之前不是説代碼長了易出現值被後面的邏輯覆蓋,那麼就以某個最小參數為一個類,所有對他的增刪改都控制在一個類,完成對某參數、對象的封裝控制。而不要散落各地去修改一個參數的值。

八 帶中斷的長邏輯處理

最後一個問題,如果帶有流程中斷的情況。如圖,一個長邏輯,在某些條件被中斷了,中斷後後面的邏輯自然是走不到了。那麼之前的對接口集合遍歷方式還能用嗎?

自然是可以用的,不過就要稍加改造,讓實現類的方法返回一個boolean值,當false時,中斷這個循環流程即可,這樣後面的邏輯就走不到了。

如果不是要中斷,而是某條件下執行自己,某條件下跳過自己,這個就往上看看文章的第4段。

最後

 本文通過一些例子,描述了一些場景下對系統的改造方式,由於京東APP後台邏輯複雜,以上場景僅覆蓋了部分典型場景,未全部寫出改造點,當然還有一部分是特有的非典型問題,可能大部分用户碰不到的場景,也未寫出。

如果有遇到類似場景,可參考文中的一些方式進行處理。如果有問題,或有建議,亦可聯繫作者[email protected]