業務系統改造嘗試引入DDD,事情變得更禿然起來……
?商品中心隨著自身業務的發展,系統復雜度逐漸變高。在業務治理過程中,我們嘗試引入了DDD來輔助進行現有業務的模型重建,并在此基礎上完成了中臺服務能力的沉淀和對外提供。通過將核心業務邏輯下沉內聚,降低調用方的業務復雜度,防范邏輯腐化。
一、前言
商品中心業務主要包括商品、類目等核心數據維護,負責和支撐嚴選內部商品相關的業務協同。
在業務的快速發展過程中,系統的復雜度也不斷提升。原來的架構已經無法適應內外部的需求,因此從17年開始,商品中心逐步經歷了管理后臺拆分、商品中心服務化、商品數據遷移等工作,并且在不斷優化,以適應嚴選日益增長的業務量。
19年開始,嚴選開始進行中臺化架構升級。我們嘗試引入了DDD來輔助進行現有業務的模型重建,并在此基礎上完成了中臺服務能力的沉淀和對外提供。通過將核心業務邏輯下沉內聚,降低調用方的業務復雜度,防范邏輯腐化。
當前,商品中心已經構建了一工作臺一中臺兩個查詢服務的系統架構。
本文將介紹中臺服務建設的相關過程、踩坑記錄,同時給需要進行類似嘗試的開發童鞋一定的參考和借鑒。
二、系統的痛點有哪些?
眾所周知,軟件系統總是在不知不覺之間變得龐大,如果沒有及時干預,系統的脈絡就會和毛線球一樣難以解開。
商品中心目前主要有以下幾點突出問題:
1、業務邏輯重復
當采用自然式的流程開發,在初期業務不復雜時,系統業務脈絡還比較簡單。一旦經歷了多個版本以上的迭代,腳本式開發的代碼不斷增刪內容,甚至出現同一段代碼拷貝后微調了部分邏輯,造成邏輯的冗余,從而增加理解和迭代的成本。
舉例商品的創建鏈路:
- 開發完成后自動創建商品
- 工單審核后自動創建純組合裝商品
- 直接創建特殊免立項商品
- 采購遷移后臺創建商品
因為前期沒有從模型的能力視角進行設計,且由于商品創建的部分差異和中途經歷了不同的同事開發等原因,結果是項目中存在多分類似又有一定差異的代碼,導致維護變得越來越困難,出了一些漏改導致的BUG。
2、模塊耦合太重
商品工作臺作為業務進行商品管理的入口服務,承擔了大量業務協同和商品數據配置的內容。由于沒有隔離,導致業務協同和商品管理的邏輯耦合太深,不利于各模塊的復用和優化。
三、DDD簡介
領域驅動設計(domain-driven design),是指通過設計領域模型,來驅動軟件設計,最終指導代碼落地的過程。一個業務領域劃分為若干個限界上下文(Bounded Context),領域模型處于各自的限界上下文之內。
DDD的具有以下特點:
- 更明確的邊界
DDD的設計原則,是使系統的邊界更加清晰,讓我們本能的進行軟件系統的分而治之。這是其最具價值的地方,當我們把問題分的越小,它的解決也越簡單。
- 更通用的語言
當邊界確定后,邊界內的術語(名詞對象、動作等),在產品、開發、測試的共同努力下,將形成具有共識的通用語言。特別是可以在后續的迭代保證這些術語是通用的。這里特別提到了通用語言的確定不再只由開發人員來決定了,是業務相關人員的共識,也更能加深大家對領域模型的了解。
- 更內聚的邏輯
一個明確的問題域中,子問題都會落到邊界內負責處理,邏輯更加內聚,對外界隔離。
作為業務研發人員,本質是通過技術更好實現業務價值。面對當前系統中的問題,我們希望DDD能在系統改造的過程中發揮它的作用。
四、系統改造之路
商品中臺服務搭建,核心思路是:抽取核心業務邏輯->抽象流程->標準化能力。
雖然是對現有業務的改造,但是在系統建模的流程上我們盡量趨向于從頭開始設計。這樣做的好處是:可以盡可能避免受到現有表結構設計的干擾。
沿用DDD的經典步驟如下:
1、戰略設計
在這過程中,主要是明確系統的通用語言。
例如我們在商品中心服務的設計過程中,我們明確了系統模型和行為,劃分子域,并編制了限界上下文和上下文映射圖,形成了包括產品和開發在內所普遍認可的通用語言。
子域又分為幾類:核心域、支撐子域、通用子域;其中核心域是整個業務域的主要成員。支撐域不是核心,負責一些具體的業務。如果支撐域可以適用整個系統,那么就變成通用域。
商品中心主要通過兩個步驟完成戰略設計:
- 領域愿景說明(Domain Vision Statement)
由產品技術等人員闡述商品域的核心能力:商品板塊負責商品管理:包括商品和SKU等核心數據維護、商品相關配置,負責和支撐嚴選內部商品相關的業務協同:包括新品開發全流程(新品立項、尋源、報改價、采購側工單、包裝設計、上線信息評審等)、售價變更、重新售賣等。
- 突出核心(Highlighted Core)
通過對業務的梳理,抽出核心模型:SPU、SKU、物理類目、配送區域、營銷配置、售后地址、服務政策等,并將這些模型按聚合關系劃分為四個子域。
2、戰術設計
戰術設計是將戰略設計進行具體化,這個過程中將明確各子域的聚合、實體、值對象、領域實踐、領域服務等,不做詳細展開,詳細可參考《實現領域驅動設計》(沃恩?弗農 (Vaughn Vernon)) 。
在上述階段,可以通過OOAD(面向對象分析方法)、四色建模、事件風暴等方式。
在我們的項目中,我們通過事件風暴識別領域事件、命令,并完成了邊界、聚合的劃分。
事件風暴:簡單概括就是通過X主體,執行了A命令,產生了B事件的這樣一個流程,來梳理核心的業務流程和規則,輸出業務對象,并推導出相關的領域模型。常用模型如下:
- Entity(實體)
每個實體是唯一的,允許狀態發生變化,但是一定有唯一標識。
- ValueObject(值對象)
值對象用于描述實體,值對象和實體的區別是不需要感知唯一標識。
- Aggregate(聚合)
聚合是一種特殊的實體,是由一組與強相關的實體和值對象組合而成的。
- Bounded Context(限界上下文)
用來封裝通用語言和領域對象,通常一個子域對應一個限界上下文。
領域對象表梳理如下:
3、編碼流程
本節主要介紹商品中心在實施過程中對一些場景的改造方法。
1)重復流程的抽象方法
在代碼改造中,可以發現有些分散在不同位置卻干著相同流程的代碼。對于這部分代碼,我們需要抽象出業務流程,并對其進行邏輯的統一收攏。
嚴選的作為品牌電商,自營立項流程是我們區別行業所特有的業務,舉例現狀分析中提到的商品創建流程,通過三步驟(梳理流程列表-->標記共同流程-->輸出通用流程),分析業務邏輯,找到核心鏈路如下:
2)核心邏輯的抽象方法
在改造中也會遇到流程較長的鏈路,我們需要獲取到核心節點,排除非核心節點,從而輸出該業務的鏈路。通過三個步驟進行:
① 梳理業務節點
② 抓取關鍵節點
在對鏈路分析后,按領域模型相關性,標記核心節點(綠色部分),形成簡化版的節點圖:
③ 收斂邏輯
在這個過程中,需要對節點進行重構,主要進行節點合并和節點異步化兩塊內容:
- 節點合并:校驗節點,我們可以歸位一個統一節點,實際業務操作的節點也按聚合合并。
- 非阻塞流程異步化:通過分析,其實有些操作是可以異步化的。例如:操作人、操作日志、上架任務的取消、緩存刷新等可以在消息通知訂閱后處理,從而繼續簡化核心鏈路。
實際上核心邏輯被我們分解成了四個階段:
通過這些過程,我們對核心流程進行了邏輯重構,從結果上看,核心邏輯下沉到中臺服務內,減少接入方的邏輯處理和代碼量,使維護性得到了較大的提升。在商品創建這個案例中,光代碼量上就縮減了2/3左右,長遠看,會降低未來迭代時邏輯梳理的時間和人力成本。
五、相關設計
1、服務架構
在嚴選中臺建設初期,我們為后續的應用架構標準進行了激烈的討論,對比經典四層架構、COLA(整潔結構)之間的優缺點。
雖然經典架構更直觀,但鑒于COLA對領域模型為中心的設計,保證領域層的獨立性,最終促成我們采用COLA架構作為統一的模版,和六邊形架構類似,它的核心理念是:應用是通過端口與外部進行交互的,內部業務邏輯(應用層和領域模型)與外部資源(外部服務,數據庫資源、消息中間件等)相互隔離,僅通過適配器進行交互。
有別于傳統的用戶界面、接口層、邏輯層、持久化層的從外到內的分層模型,這是種全新的思想,我們認為用戶界面、數據庫、消息等都屬于平等的外部方,他們都需要通過端口和應用交互,COLA的思想中,更加突出了架構核心是領域模型。
商品中臺服務在此基礎上的系統分層如下:
2、事件機制
場景:修改SKU售價,被我們打包成一個具體的能力如下:
上述場景中,應用層調用“更新售價”的領域行為后,還要處理其他行為,等所有行為處理完成后事件才對外發送。
因此我們的消息機制需要滿足以下要求:
- 事件提交:允許事件提交到當前場景。
- 事件異步和控制:指的是可以控制事件實際對外通知的時機。
- 事件異常重試:異常后支持重試實現業務補償。
消息初始化流程如下:
系統初始化邏輯:
- 容器啟動
- 完成Listeners實例創建
- 初始化EventBus事件總線
- 從BeanFactory掃描所有Listeners
- 注冊Listeners到EventBus事件總線
實際消息處理流程:
在應用層處理完所有業務并完成事務提交后,系統將暫存在EventContext中的消息post到異步處理的eventBus中,由eventBus協調消息的投遞和處理。
如果是內部消息,直接由訂閱者處理,如果是外部消息,由訂閱者通過MQ轉發至外部。
當然,我們需要支持事件定制的消息異常處理機制,對于有需要重試的消息,允許在啟動時注冊重試處理器。
六、討論
在實踐中,也發現一些問題和解決思路供大家參考。
1、實體范圍大小問題
在設計中,我們存在把同一類屬性歸為一個實體,但是實際使用中,會發現對該實體的使用仍然是按模塊的。例如履約實體:
設計之初,我們將商品的預約配送、發票開關、稅率等都歸為履約實體,但在實際應用中,實際還是按子業務配置,那么在對實體更新的處理邏輯中,需要過多關注屬性覆蓋等問題,這就是沒有拆分完全,履約實體內其實可以再拆分為三個實體。因此設計時不光要考慮屬性的相似性,更要結合業務場景進行設計。
2、模型的開發模式選擇
首先明確分類,因為這里會出現不同時期對貧血模型的定義歧義,導致大家理解的不同。
- 失血模型:實體只有setter/getter。
- 貧血模型:domain ojbect包含了不依賴于持久化的業務邏輯。
- 充血模型:絕大多業務邏輯都應該被放在domain object里面(包括持久化邏輯),而Service層應該是很薄的一層。
- 脹血模型:取消Service層,只剩下domain object和DAO兩層,在domain object的domain logic上面封裝事務。
在這種定義下,充血和貧血其實各有好處,本質上我認為依然負責領域模型包含業務的思想。我們采用貧血模式,在保留模型業務邏輯的同時,不希望引入持久化邏輯,兼顧開發接受度和模型的整潔度。
3、服務粒度
微服務粒度建議按子域范圍拆分。如果拆分太細,需要考慮分布式事務問題,增加了復雜度(特別是對于一些核心域有數據一致性要求的場景)。
七、結尾
本文主要介紹了商品中心中臺服務創建過程中的DDD實踐思路、業務改造案例、服務架構設計、消息機制等內容。希望其中的一些類似案例和實施手段可以為后續其他產品線實踐提供一個探索思路。
當然,后端服務的架構演進遠非如此簡單。在完成現有業務的改造后,依然會面臨業務變更和新增業務的挑戰,系統的模型也不是一成不變的,我們也依然需要對系統進行不斷的自我更新以適應業務的發展。?