前言
天貓優品導購歸因鍊路負責天貓優品訂單導購判定工作,目前支撐了天貓優品權益券導購、普通導購和淘花導購等多種導購類型。随着業務疊代,現有導購歸因鍊路在維護性、擴充性和可讀性等方面存在明顯不足,代碼複雜性不斷攀升,曆史代碼債務逐漸積累。
為解決上述問題,開展了天貓優品導購歸因鍊路技術重構工作。進一步地,在導購歸因重構基礎上,作為對“屬性-分類-執行”問題的産品化思考與實踐,提出了一種通用歸因技術元件 ACE 。ACE 元件基于屬性校驗器、分類器和執行器三層模型解決了屬性分類的通用性問題,具有良好的擴充性和代碼語義。
本文以天貓優品導購歸因重構為背景,闡述了一種基于 ACE 元件的訂單歸類技術方案。
技術痛點
伴随業務快速上下線,現有天貓優品導購歸因鍊路在不斷疊代過程中逐漸積累曆史代碼債務,應用代碼存在複雜性高、擴充性低、可讀性差等問題。
▐ 事務腳本程式設計
事務腳本程式設計導緻代碼複雜性攀升。事務腳本型代碼可能我們每天都在寫,我們有時在用一門面向對象語言寫着面向過程代碼,基于 IF-ELSE 等條件判斷語句快速堆砌業務代碼。每新增一行業務代碼,也許就新增了一行代碼債務,應用代碼複雜性逐漸攀升。

圖 1 :代碼複雜性的演變
▐ 違背開閉原則
違背開閉原則導緻可維護性差。每次業務需求疊代,都在原有業務代碼基礎上修改或新增邏輯。我們很難知道曆史代碼哪些地方埋了坑,最好的方式就是盡量避免改動它。實際上,對于大部分業務代碼,很難保證在新增需求時完全不需要改動原有代碼邏輯。
▐ 缺失架構設計
缺失架構設計導緻可擴充性差。業務型技術團隊常常面臨業務需求急、開發周期短等問題,為支撐新業務快速上線,有時會采取最快的方式滿足業務訴求。然而,最快的方式往往缺失架構設計,隻為滿足單一需求,對後續疊代并不友好。随着源源不斷的新需求,應用代碼很快陷入破窗效應,可擴充性越來越差,代碼債務不斷積累。
▐ 業務邏輯複雜
複雜業務邏輯導緻代碼可讀性差。看到下面這段代碼,可能很難了解滿足哪些屬性是導購訂單。這樣的代碼在業務型技術團隊很常見,我們帶着業務需求打開應用代碼,卻發現連原有代碼所表示的業務含義都難以了解。代碼可讀性對業務型技術團隊尤為重要,因為代碼往往隐藏着業務含義,複雜的業務場景加上晦澀難懂的應用代碼無疑是雪上加霜。
圖 2 :晦澀難懂的業務代碼邏輯
重構目标
為解決上述技術痛點,結合天貓優品導購業務發展背景,制定以下重構目标。
▐ 精簡導購歸因鍊路,清理過時業務邏輯
業務發展存在不斷試錯的過程,應用代碼伴随着業務不斷疊代。有些業務代碼雖然早已過時,且由于團隊開發人員流動,誰也不敢輕易删除曆史代碼。代碼上線容易下線難,代碼愈發臃腫。是以,有必要精簡現有天貓優品導購歸因邏輯,清理過時業務邏輯。
▐ 抽象業務模型,向後相容業務發展
結合現有業務場景,抽象業務模型,支援後續業務輕量化疊代。通過抽象業務模型,可降低應用代碼複雜度與業務場景複雜度的強相關性,甚至實作同一模型支撐多種不同業務場景。
▐ 完善業務優先級決策,規則統一收口
規範業務優先級決策,統一收口業務優先級規則,便于後續代碼維護和業務疊代。優先級決策是一種很常見的業務規則。如何用一行代碼描述所有業務優先級,而不是将業務優先級判斷散落在應用的多處地方?
▐ 提升代碼可讀性,代碼語義即業務語義
借助通用業務模型,賦予代碼更豐富的語義,提升代碼可讀性。代碼可讀性對業務型技術團隊尤其重要,看懂代碼即看懂業務規則,可極大減少溝通成本,提升開發效率。
技術方案
基于現有技術痛點和重構目标,首先抽象業務模型,然後設計了一種通用歸因技術元件 ACE,最後将 ACE 應用于天貓優品導購歸因鍊路。
▐ 業務模型
以導購歸因為例,導購歸因旨在判斷一個訂單存在哪種類型的有效導購行為。有效性定義可概括為兩個方面:一是滿足或過濾某些屬性,二是滿足業務優先級規則。
導購歸因是訂單歸類和優先級決策的組合,具體概括為以下四個步驟:
Step 1 :導購訂單必須滿足或不滿足某些屬性
例如,天貓優品導購訂單必須滿足天貓優品商品等屬性,且不滿足(過濾)本地履約訂單等屬性。
Step 2 :不同屬性組合成不同類型導購訂單
例如,權益券導購訂單 = 天貓優品商品訂單 + 權益券訂單 + ... + 非本地履約訂單 + 非定向優惠訂單。
Step 3 :不同導購訂單類型存在不同業務優先級
根據優先級規則決策哪種類型導購訂單有效。例如,權益券導購訂單優先級高于普通導購訂單。
Step 4 :根據歸因結果執行不同處理流程
例如,訂單判定為導購訂單,執行落庫、打标、消息推送等流程。
進一步地,導購歸因可抽象為“屬性-分類-執行”問題,抽象模型如下:
屬性校驗器(Attributor):表示一種屬性。校驗是否滿足某個屬性,支援原子或組合屬性。
分類器(Classifier):表示一種類型。綁定一個或多個屬性校驗器,校驗是否滿足某些屬性組合。分類器可分為嵌套分類器(NestedClassifier)和原子分類器(AtomicClassifier)。例如,Classifier 1 需要滿足多個 Attributor,Classifier 4 需要滿足 Classifier 1 和 Classifier 2。
執行器(Executor):表示一種類型對應的執行政策。綁定一個分類器,負責對某種類型執行處理。
圖 3 :模型層次結構 Attributor-Classifier-Executor
▐ 歸因元件
基于現有業務場景,抽象了一種“屬性-分類-執行”的技術模型。在通用模型基礎上,設計了一種歸因元件 ACE 。ACE 是 Attributor - Classifier - Executor 的縮寫,旨在通過屬性校驗器(Attributor)、分類器(Classifier)和執行器(Executor)三層模型解決屬性分類的通用性問題。
整體設計
ACE 對外暴露統一服務接口 AceWorker,AceWorker 接收外部傳入參數(歸因場景 + 歸因對象),根據歸因場景擷取分類器,并判斷歸因對象是否滿足該分類器。分類器是 ACE 的核心,綁定了一個或多個屬性校驗器,并對應唯一的執行器。
圖 4 :ACE 整體設計
詳細設計
ACE 元件由 ACE 注解、ACE 工廠容器、ACE 初始化和 ACE 服務入口組成,詳細設計如圖 5 所示。
ACE 注解
基于易用性考慮,ACE 提供三種注解 @Attributor、@Classifier 和 @Executor 用于聲明 ACE 元件,分别對應屬性校驗器、分類器和執行器。
- @Attributor:聲明一個屬性校驗器,屬性校驗器名稱唯一。
- @Classifier:聲明一個分類器,分類器名稱唯一。@Classifier 提供 matcher、filter 和 priority 三種屬性,matcher 用于指定該分類器需滿足的屬性校驗器清單,filter 用于指定該分類器需過濾的屬性校驗器清單,priority 用于指定該分類器綁定的原子分類器的優先級規則。
- @Executor:聲明一個執行器,每個分類器對應一個執行器,執行器名稱需與分類器名稱一緻。
ACE 工廠容器
AceFactory 是 ACE 的工廠容器,負責管理所有定義的 ACE 元件,包括屬性校驗器集合、分類器集合及其綁定的屬性校驗器集合、執行器集合。根據 ACE 元件名稱可直接從 AceFactory 擷取對應的 ACE 元件。
ACE 初始化
借助 AceInitService 初始化 ACE 元件,應用啟動時 AceInitService 自動解析 ACE 注解,并将 ACE 元件注冊到 ACE 工廠容器。
ACE 服務入口
AceWorker 是 ACE 的服務入口,負責對外提供 ACE 通用服務,如屬性校驗 attribute、分類 classify 和執行 execute 。
圖 5 :ACE 詳細設計
示例
1)定義屬性校驗器
定義屬性校驗器 A ,判斷是否滿足屬性 A 。
/**
* 屬性校驗器示例 ATTRIBUTOR_A
* @author haoyu.chy
* @date 2020/9/5
*/
@Attributor(name = "ATTRIBUTOR_A")
public class AttributorA implements IAttributor {
@Override
public AceResult attribute(AceContext aceContext) {
if (滿足屬性A) {
return new AceResult(true);
}
return new AceResult(false);
}
}
2)定義分類器
定義原子分類器 CLASSIFIER_X(綁定屬性校驗器 ATTRIBUTOR_A ),判定是否滿足類型 X 。
/**
* 原子分類器示例
* matcher:需比對的屬性
* filter:需過濾的屬性
* @author haoyu.chy
* @date 2020/9/5
*/
@Classifier(name = "CLASSIFIER_X", matcher = "ATTRIBUTOR_A", filter = "")
public class ClassifierX implements AtomicClassifier {
}
定義嵌套分類器 CLASSIFIER_NEST ,綁定原子分類器 CLASSIFIER_X 和 CLASSIFIER_Y ,CLASSIFIER_X 優先級高于 CLASSIFIER_Y 。
/**
* 嵌套分類器示例
* priority:表示綁定的原子分類器的優先級規則
* @author haoyu.chy
* @date 2020/9/5
*/
@Classifier(name = "CLASSIFIER_NEST", priority = "CLASSIFIER_X,CLASSIFIER_Y")
public class Classifier_Nest implements NestedClassifier {
}
3)定義執行器
定義原子分類器 CLASSIFIER_X 對應的執行器 ExecutorX 。
/**
* 執行器示例
* 注:執行器名稱對應分類器名稱
* @author haoyu.chy
* @date 2020/9/5
*/
@Executor(name = "CLASSIFIER_X")
public class ExecutorX implements IExecutor {
@Override
public AceResult execute(AceContext aceContext) {
// do something ...
return new AceResult();
}
}
4)定義服務入口
定義歸因服務入口,指定歸因場景 aceScene 和歸因對象 aceObject,借助 AceWorker 完成歸因工作。
/**
* 歸因服務入口示例
* @author haoyu.chy
* @date 2020/9/5
*/
public class SimpleService {
public static void main(String[] args) {
// 歸因場景(例如,CLASSIFIER_NEST)
String aceScene = "CLASSIFIER_NEST";
// 被歸因對象(例如,訂單号)
Long aceObject = 1234567890L;
// 初試化上下文
AceContext<Long> aceContext = AceContext.of(aceScene, aceObject);
// 執行歸因流程
AceResult aceResult = AceWorker.getInstance().classify(aceContext);
}
}
▐ 天貓優品導購歸因
天貓優品導購訂單類型舉例:
- 權益券導購訂單:訂單存在權益券使用記錄
- 天貓優品普通導購訂單:天貓優品商品且最近一次導購記錄為普通導購
-
天貓優品淘花導購訂單:天貓優品商品且最近一次導購記錄
為淘花導購
不同類型訂單有不同優先級,業務規則如下:
- 優先級規則:權益券導購優先級最高,普通導購和淘花導購優先級并列(最近原則)
- 互斥規則:天貓優品定向優惠訂單、本地履約訂單、代購訂單優先級高于所有類型導購訂單
基于 ACE 元件,重構天貓優品導購歸因技術鍊路,設計天貓優品導購歸因流程如圖 6 。
圖 6 :天貓優品導購歸因流程
Step 1 :定義屬性校驗器
屬性校驗器互相獨立,表示是否滿足某種屬性。例如,定義屬性校驗器(Attributor):天貓優品商品标、權益導購券、普通導購記錄、淘花導購記錄、定向優惠、本地履約等。
Step 2:定義原子分類器
原子分類器綁定多個屬性校驗器,表示是否滿足某種類型。例如,定義分類器(Classifier):權益券導購訂單(COUPON_GUIDE)、普通導購訂單(NORMAL_GUIDE)和淘花導購訂單(SUPERB_GUIDE)。其中,權益券導購訂單(COUPON_GUIDE)綁定權益導購券屬性,過濾定向優惠和本地履約等屬性。
注:圖 6 實線表示滿足,虛線表示過濾
Step 3:定義嵌套分類器
嵌套分類器綁定多個原子分類器,一般用于優先級決策場景,按前後順序比對第一個有效分類器。例如,定義嵌套分類器(GUIDE_ORDER),其綁定原子分類器(COUPON_GUIDE、NORMAL_GUIDE 和 SUPERB_GUIDE),優先級從前往後。
Step 4:定義執行器
每個原子分類器對應一個執行器。如果某個分類器有效,則執行對應的執行器。
Step 5:定義服務入口
定義 ACE 元件後,隻需指定歸因場景(對應分類器名稱)和歸因對象,即可使用歸因服務。例如,歸因場景為導購歸因(GUIDE_ORDER),歸因對象為某個訂單,則表示對某個訂單執行導購歸因。
基于 ACE 元件,定義屬性校驗器、分類器和執行器,封裝服務接口。天貓優品導購歸因代碼架構如圖 7 。
圖 7 :天貓優品導購歸因代碼架構
效果分析
▐ 遵循 SOLID 原則
單一責任原則(SRP):每個 ACE 元件隻負責一種職責。Attributor 隻判斷是否滿足某種屬性,Classifier 隻判斷是否滿足某種類型,Executor 隻對某種類型執行處理。
開放關閉原則(OCP):ACE 元件之間互相獨立。新增屬性或分類無需修改原有元件,隻需定義一種新的屬性校驗器或分類器即可,完全無需改動原有代碼。
裡氏替換原則(LSP):父類可用子類替代,子類隻擴充父類方法,不重寫父類方法。ACE 基于接口實作,不重寫已實作方法。
接口隔離原則(ISP):子類不被迫依賴它不需要的方法。ACE 元件接口互相獨立,且隻提供唯一方法。
依賴倒置原則(DIP):ACE 元件面向接口程式設計,基于 ACE 工廠容器實作依賴注入,元件間不存在直接依賴關系。
舉例:
如圖 8 所示,新增導購類型(優盟導購),隻需新增分類器 Classifier(UM_GUIDE),并綁定相關屬性 Attributor(優盟),關聯執行器 Executor(優盟導購)。借助 ACE 元件可無侵入性地新增業務類型,完全無需改動原有代碼。
圖 8 :新增導購類型(優盟導購)
▐ 代碼結構優化
代碼結構從原有的「縱向+多出口」轉變成「橫向+單出口」。從代碼維護角度,單出口程式更利于維護,代碼(方法)出口統一收口到一處地方,代碼邏輯一目了然。
圖 9 :橫向單出口的代碼結構
總結
本文提出了一種通用歸因技術元件 ACE,并将其應用于天貓優品導購歸因技術鍊路。ACE 元件以屬性校驗器、分類器和執行器三層模型為核心,規範化定義某種類型需滿足的屬性組合及其對應的執行政策。
ACE 元件借鑒了政策模式思想,不同分類器對應不同執行器(政策),但隻有分類器有效時,相應執行器(政策)才會被執行。分類器支援組裝式關聯多個屬性校驗器,同一屬性校驗器可被多個分類器複用,具有較好的靈活性和擴充性。此外,ACE 元件可将代碼邏輯結構化,提升應用代碼的可讀性。
思考
一周歲技術新人的非嚴謹思考
“業務需求的局部性原理”
大家都聽過計算機系統的局部性原理,其實業務需求也存在“局部性原理”。小到多打一行日志、多傳一個參數,大到多留一個擴充點、抽象一個服務,這都在為後續需求留餘地。寫代碼時多做一步,不寫一次性代碼,也許反而能減少後續工作量。
“應用代碼的破窗效應”
在實際需求開發過程中,我們往往會參考原有代碼實作。代碼風格或結構設計是具有傳染性的,糟糕的代碼風格和架構設計會使應用陷入破窗效應。一個不成熟的思考,是否絕大部分應用都難逃破窗效應?即使前期有較好的架構設計,但由于業務發展和人員流動,原有架構限制依然有可能過時或被忽略。
“業務團隊的技術挑戰”
在集團的“關懷”下,業務型技術團隊的技術越做越輕,業務越做越重。開箱即用的中間件,讓業務團隊變得“沒有技術挑戰”。剛參加工作的這一年,常常焦慮個人技術成長。回過頭想,對于業務團隊而言,技術挑戰也許在廣不在深。大多數情況下,技術型團隊或許在和機器打交道,考驗技術深度與鑽研能力;業務型團隊則在和商業打交道,考驗技術架構與抽象能力。孰好孰壞似乎沒有絕對答案。
淘系技術部-天貓優品團隊
阿裡集團新零售戰略闆塊的重要一環,圍繞消費電子/家裝等領域,以天貓優品數字化門店為核心,沉澱一套從供給端到消費端的全鍊路數字化解決方案。
如您在尋求新的工作機會,歡迎投遞履歷至:[email protected] ,期待您的加入!
關注「淘系技術」微信公衆号,一個有溫度有内容的技術社群~