1. 程式人生 > >領域驅動設計在馬蜂窩優惠中心重構中的實踐

領域驅動設計在馬蜂窩優惠中心重構中的實踐

前言

正如領域驅動設計之父 Eric Evans 所著一書的書名所述,領域驅動設計(Domain Driven Design)是一種軟體核心複雜性應對之道。

在我們解決現實業務問題時,會面對非常複雜的業務邏輯。即使是同一個事物,在多個子業務單元下代表的意思也是不完全一樣的。比如「商品」這個詞,在商品詳情頁語境中,是指「商品基本資訊」;在下單頁語境中,是指「購買項」;而在物流頁面語境中,又變成了「被運送的貨物」。

DDD 的核心思想就是讓正確的領域模型發揮作用。所謂「術業有專攻」,DDD 指導軟體開發人員將不同的子業務單元劃分為不同的子領域,在各個子領域內部分別對事物進行建模,來應對業務的複雜性。

 

一、重構優惠中心的背景

我們在實際的開發過程中都遇到過這種情況,最初因為業務邏輯比較單一,為了快速實現功能, 以及對成本、風險等因素的綜合考慮,我們會為業務統一建立一個大的模型,各個模組都使用這同一個模型。但隨著業務的發展,各子領域的邏輯越來越複雜,對這個大模型的修改就會變成一種災難,有時明明是要改一個 A 子領域的邏輯,卻莫名其妙影響到了 B 或者 C 子領域的線上功能。

優惠中心就是一個例子。優惠中心主要負責馬蜂窩各業務線商品的優惠活動管理,以及計算不同使用者的優惠結果。「商品管理」和「優惠管理」作為兩個不同的業務單元,在初期被設計為共用一個商品模型,由商品模組統一管理。

圖1 :初期商品模型

 

出現的問題

隨著業務的發展,優惠的形式不斷推陳出新,業務形態逐漸多樣,業務方的需求也越來越個性化,導致後期的優惠中心無論從功能上還是系統上都出現了一些具體的問題:

1. 功能上來說,不夠靈活

優惠資訊是作為商品資訊的一個屬性在商品管理模組配置的。比如為了引導使用者使用 App 需要設定 A 型別優惠,就通過在商品資訊的編輯頁面增加一個 A 型別優惠配置項實現;如果某個商品的 A 型別優惠需要在 0:00 分生效,業務同學就必須在電腦前等到 0:00 更新商品資訊來上線優惠活動。

另外,如果想要建立針對所有商品都適用的優惠,按照之前的模式,所有的商品都要設定一遍,這幾乎是不可接受的。

2. 從系統層面看,不易擴充套件

優惠資訊儲存在商品資訊中,優惠資訊是通過商品管理模組的介面輸出的。如果要新增一種優惠型別,商品資訊相關的表就要增加欄位,商品的表會越來越大;如果要迭代一個優惠的邏輯,就有可能影響到商品管理模組的功能。

3. 不利於迭代

由於優惠資訊僅僅作為商品的一個屬性,沒有自己的生命週期,所以很難去統計某一次設定的優惠的投入產出比,從而指導後續的功能優化。

重構優惠中心的預期

  • 系統層面上,要把優惠相關的業務邏輯獨立出來,單獨設計和實現;

  • 應用層面上,優惠中心會有自己的獨立後臺,負責管理優惠活動;也會有獨立的優惠計算介面,負責 C 端使用者使用優惠時的計算。

 

二、分什麼選擇 DDD

避免貧血模型

基於傳統的 MVC 架構開發功能的時候,Model 層本質上是一個 DAO 層,業務邏輯通常會封裝在 Service 層,然後 Controller 通過呼叫 Service 層來完成對外的功能。這種模式下,資料和行為分別被割裂到了 Model 和 Service 兩層。我們把這種只承載資料,但沒有業務行為的 Model 稱為「貧血模型」。

我們在和業務方瞭解需求的過程中,使用到的物件都是現實業務的對映,是行為和屬性的綜合體。需求確定好之後,我們開發的過程中,人為把行為和資料拆分成了兩部分,做了一次轉換。隨著需求的迭代,人員的更迭,開發看到的程式碼和業務方的需求越來越對應不上,導致很多程式碼誰也不知道對應的是什麼業務邏輯,這種現象被稱為由貧血模型帶來的「失憶症」,最終導致的是一個維護成本極高的大泥潭系統。

領域驅動設計的核心就是基於業務邏輯去建模,避免貧血模型,減少設計和開發過程中對業務資訊的丟失和轉換。在業務邏輯迭代的過程中,系統通過調整對應的業務模型就可以完成迭代。

 

三、落地過程

關鍵點:業務邏輯抽象

要做到基於業務邏輯建模,就要合理地抽象。因為業務表象千差萬別,產品經理和軟體設計人員需要和業務專家深入交流,並且從離散的資訊中抽象出業務內在的邏輯。

比如旅遊業務售賣的商品和標品不同,有些優惠是不考慮人群的,比如使用優惠券,所有型別的庫存都可以享受;但如 N 人 N 折這類優惠,成人價可以享受,兒童價和單房差就不可以。基於這個特點,我們對優惠中心的商品模型做了抽象,抽象出來「是否可以參與件數計算」和 「是否可以參與價格計算」兩個通用屬性。這樣既實現了基於業務邏輯建模,又不會陷入業務邏輯千差萬別的表象中。

3.1 戰術設計

第一步:統一語言,提煉關鍵詞

準確的語言對於產品、運營、開發等各方對齊需求非常重要,我們需要將優惠邏輯當中的概念抽象為各方都能理解的詞語,以達成共識。作為開發人員來說,對領域的理解一般來說是比較少的,為了抽象出合理的語言讓產品和業務方都能理解,就需要充分理解業務背景和需求。在熟悉業務和需求的過程中,提煉出若干關鍵字,這些關鍵詞就是最初產生的領域概念和通用語言。比如:

  • 優惠型別:表示一種優惠規則和對應的優惠方案。比如早鳥優惠,就是早多少錢買(優惠規則),減多少錢/打幾折(優惠方案);

  • 優惠活動:擁有完整的生命週期,需要包含時間、平臺、人員、商品等(限制維度)的某種優惠型別的使用過程資訊;

  • 優惠發現:根據指定的商品、人員和平臺,找出可以使用的優惠活動列表服務;

  • 優惠計算:根據指定的商品、人員、平臺以及購買數量,計算出這一次購買行為可以享受的優惠金額及優惠明細;

  • 優惠排序:各種優惠型別在計算的時候是有先後順序的,如果有打折的優惠存在,那順序不同,計算的結果也會不同;

  • 優惠互斥:某些優惠之間存在互斥的關係,比如使用了金卡 96 折優惠,就不能使用馬蜂窩優惠券。

第二步:抽象領域模型

根據單一職責的原則,一個領域概念對應一個領域物件。領域物件有實體值物件之分:

  • 實體:實體是有狀態的和唯一標識的,包含屬性和行為;

  • 值物件:值物件是無狀態的,是隻讀的,包含屬性和行為。

區分實體和值物件對系統設計有很大意義,實體是我們需要重點關注和設計的,而值物件則只使用它的「值」就可以了。這樣可以簡化系統的複雜度,將精力聚焦在核心領域物件。不難理解,優惠活動毋庸置疑是一個實體,優惠型別就是一個值物件。

但也存在某些業務行為是不能歸於某個實體或值物件的,可以將它們歸為領域服務:

  • 領域服務:領域服務本質上就是一些操作,不包含狀態,通常用於協調多個實體。實體和值都屬於領域物件,領域物件之間的互動邏輯不能放在領域物件內部,必須由服務來實現,從而有效地保護領域模型。

有一些領域邏輯,比如「優惠排序」和「優惠互斥」,他們涉及到多個優惠型別,也就是多個領域物件。如果也被設計為領域物件,就打破了單一職責的原則,所以我們把這部分跨多個領域物件的業務邏輯放到「領域服務」層。

第三步:抽象領域物件之間的關聯關係

將相關聯的領域物件進行顯式分組,來表達整體的概念(也可以是單一的領域物件),也就是「聚合」

比如優惠活動是優惠型別、優惠範圍等的聚合;優惠型別是優惠規則和優惠方案的聚合;優惠規則是限制維度的聚合;優惠方案是優惠手段的聚合:

圖2 :關聯關係示意

聚合的主要功能是把領域物件分組,外部的唯一訪問點就是聚合根,這樣可以避免處理領域物件間的一一對應關係,只需要處理聚合和聚合之間的關係就行了。

第四步:走查場景,調整領域模型

領域模型的調整是貫穿整個設計和開發過程的,隨著業務的調整,領域模型也需要調整。比如優惠中心後期引入了會員卡的優惠型別,那麼就需要把優惠券這個優惠型別的顯示,調整為與會員卡互斥的優惠券和與會員卡不互斥的兩種。

第五步:簡化設計,降低系統複雜度

建模的本質是對現實事物的一種簡化和抽象,指導我們忽略和問題域無關的事實,提取和問題域息息相關的資訊。以優惠中心為例,最初的方案裡我們設計了優惠型別管理的功能,根據不同的優惠規則和優惠方案自動組合成不同型別的優惠型別。但是可以預見,未來的優惠型別是有限的,並且每個優惠型別都有會自己的特殊配置,比如 N 人優惠裡的 每 N 人/第 N 人;早鳥中的提前 N 天等。也就是說,根據優惠規則和優惠方案自動生成優惠型別基本是沒有使用場景的,因此也就去掉了這個設計。

再如,對優惠的限制我們最初是設計在優惠活動維度,經過權衡,為了降低系統複雜度,最後實現在了優惠型別層面。以「蜂搶」優惠型別為例,它的規則是所有的蜂搶活動都是 1 個使用者只能搶一次,沒有必要把這個限制放在優惠活動維度,在優惠型別層面控制就可以了。

3.2 戰略設計

戰略設計處理的是不同限界上下文之間的拆分和整合邏輯。限界上下文比較抽象,結合我們在文章開始提到的不同語境中的「商品」例子來理解,同一個詞如果不說明白所處的語境,是無法準確描述清楚其表達的含義的。「語境」其實就是「上下文」,對應不同「子領語」。同理,如果不在一個限定好的上下文中去設計領域模型,設計出的領域模型是不清晰的,它就會同時支援多個上下文。

這裡需要說明一點,如果是從零搭建一個全新的電商系統,首先需要做的應該是戰略設計。而優惠中心是建立在現有大的電商系統基礎上,相當於作為其中一個子領域進行重構,所以我們才會先來做戰術設計,再考慮在完整的電商系統下它與外部其他環境之間的關係,也就是戰略設計。

優惠中心內部場景區分

優惠中心包括了服務於 B 端使用者的優惠活動管理和服務於 C 端使用者的優惠計算這兩個不同的子業務單元:

圖3 :優惠中心內部場景區分

  • 優惠活動處理的是優惠活動的增刪改查,以及配套的統計等業務;優惠活動在這裡是一個實體,有完整的生命週期,有上線、下線等狀態,可以被建立和刪除;

  • 優惠計算處理的是一個訂單能享受哪些優惠,並減多少錢的問題;在這個場景裡,優惠活動是一個值物件,只提供優惠計算需要的必要引數即可。

優惠中心與外部系統整合

在整個電商系統的環境下,優惠中心作為一個子域,處於自己的限界上下文當中。使用優惠中心服務的詳情頁、下單頁都處於自己各自的限界上下文,所以呼叫優惠中心的時候就需要設計它們之間的上下文對映方式。

呼叫和被呼叫方使用的戰略設計方法通常有以下幾種:

  • 客戶方-供應方:適用於同一個團隊之間的協作,上游會有嚴格的自動化測試,來保證給到下游的資料是一定符合約定的;

  • 遵奉者:適用於不同團隊協作,且上游不關心下游的標準,下游又完全「逆來順受」地接受了上游給的資料的場景;

  • 防腐層:適用於上游不關心下游的標準,但是下游不甘心「逆來順受」,就增加一層,來做轉換處理,保持下游系統的獨立性;

  • 開放主機服務:適用於中臺(通用能力平臺),對接方非常多,業務重複度高,並且已經有完善的測試機制和通用的模型。

結合我們的實際情況來看,呼叫優惠中心的可能會是不同團隊的開發人員,而優惠中心又不想被不同的上游侵入內部設計中,所以「客戶方-供應方」和「遵奉者」模型都不適合;另外優惠中心前期接入方會比較少,而且會不斷迭代,使用「開放主機服務」也不太合適。綜合考慮下,防腐層的設計比較適合優惠中心。

下圖是優惠中心的業務架構示意,中間的應用服務層採用的就是防腐層的設計,反映優惠中心與外部系統整合時的上下文對映關係:

 

圖4 :優惠中心業務架構

3.3 架構實現

優惠中心選擇的是經典的分層架構。從上到下為使用者介面層、應用服務層、領域層和倉儲層。圖中不同的顏色塊分別對映外部服務、應用服務、領域服務、聚合根、實體、值物件和倉儲。

 

圖5 :優惠中心分層領域模型

  • 使用者介面層:處理和終端使用者的互動邏輯;

  • 應用服務層:負責封裝和轉換領域層的返回資料給使用者介面層;

  • 領域層:優惠中心的核心邏輯都在這一層,包括領域物件和領域服務。

  • 倉儲層:倉儲層負責把記憶體中的領域物件落地到儲存介質,也負責從儲存介質拿到原始資料後構造領域物件給領域層使用;這一層對領域層隱藏了底層的儲存細節。雖然倉儲層處在領域層下方,但是我們實現過程中採用了依賴注入的方式,將倉儲層的具體實現注入到領域層中。

 

四、問題及近期規劃

1. 價格層優惠

現在公司面沒有一個統一的商品中心,並且各業務線對商品的定義差別很大。比如自由行的商品包括出行日期、價格類別(成人價、兒童價)和套餐類別等層級;而火車票的商品包含座次、席別、目的地和出發地等層級。

如果優惠中心抽象出一種通用的商品層級來適配各個業務線,那實際上就是優惠中心要對商品進行標準定義,但是這個標準與後續商品中心的標準定義很有可能是不一致的,如果不一致優惠中心就要做大的改版。所以最終的解決方案可能還要通過推進統一商品中心的建立來解決。

2. 效能問題

領域驅動設計帶來的弊端就是類的增多。目前優惠中心的技術棧基於 PHP, PHP 是一種解釋型語言,在DDD 模式下即使有了 OPCode 等快取技術,執行階段的耗時相對其他靜態資料型別的語言還是較大。所以後面計劃將優惠中心使用 Java 技術棧重構,來進行效能上的優化。

 

五、小結

本文介紹了馬蜂窩電商優惠中心基於 DDD 進行重構的一些實踐經驗。DDD 的思想也幫助我們在業務迭代的過程中將架構設計得更加合理。

當然,是否採用業務驅動設計的思想,需要取決於業務和團隊的實際情況。在馬蜂窩業務的快速發展下,我們在架構設計上還將做更多的探索,也將持續與大家交流。

本文作者:徐興旺,馬蜂窩電商研發平臺服務團隊技術專家。

(馬蜂窩技術原創內容,轉載務必註明出處儲存文末二維碼圖片,謝謝配合。)

相關推薦

領域驅動設計馬蜂窩優惠中心重構實踐

前言 正如領域驅動設計之父 Eric Evans 所著一書的書名所述,領域驅動設計(Domain Driven Design)

領域驅動設計在網際網路業務開發實踐

前言 至少30年以前,一些軟體設計人員就已經意識到領域建模和設計的重要性,並形成一種思潮,Eric Evans將其定義為領域驅動設計(Domain-Driven Design,簡稱DDD)。在網際網路開發“小步快跑,迭代試錯”的大環境下,DDD似乎是一種比較“古老而緩慢”的

領域驅動設計的聚合根和實體

1.聚合根、實體、值物件的區別? 從標識的角度:   聚合根具有全域性的唯一標識,而實體只有在聚合內部有唯一的本地標識,值物件沒有唯一標識,不存在這個值物件或那個值物件的說法; 從是否只讀的角度:   聚合根除了唯一標識外,其他所有狀態資訊都理論上可變;實體是可變的;值物件是隻讀的; 從生命週期的角

狀態模式在領域驅動設計的使用

領域驅動設計是軟體開發的一種方式,問題複雜的地方通過將具體實現和一個不斷改進的核心業務概念的模型連線解決。這個概念是Eric Evans提出的,http://www.domaindrivendesign.org/這個網站來促進領域驅動設計的使用。關於領域驅動設計的定義,http://dddc

關於領域驅動設計(DDD)聚合設計的一些思考

關於DDD的理論知識總結,可參考這篇文章。 DDD社群官網上一篇關於聚合設計的幾個原則的簡單討論: 聚合是用來封裝真正的不變性,而不是簡單的將物件組合在一起; 聚合應儘量設計的小; 聚合之間的關聯通過ID,而不是物件引用; 聚合內強一致性,聚合之間最終一致性; 上面這幾條原則,作者通過

從0開發3D引擎(十):使用領域驅動設計,從最小3D程式提煉引擎(上)

[TOC] 大家好,本文使用領域驅動設計的方法,重新設計最小3D程式,識別出“使用者”和“引擎”角色,給出各種設計的檢視。 # 上一篇博文 [從0開發3D引擎(九):實現最小的3D程式-“繪製三角形”](https://www.cnblogs.com/chaogex/p/12234673.html)

從0開發3D引擎(十一):使用領域驅動設計,從最小3D程式提煉引擎(第二部分)

[TOC] 大家好,本文根據領域驅動設計的成果,開始實現從最小的3D程式中提煉引擎。 # 上一篇博文 [從0開發3D引擎(十):使用領域驅動設計,從最小3D程式中提煉引擎(第一部分)](https://www.cnblogs.com/chaogex/p/12408831.html) # 本文流程 我

從0開發3D引擎(十二):使用領域驅動設計,從最小3D程式提煉引擎(第三部分)

[TOC] 大家好,本文根據領域驅動設計的成果,實現了init API。 # 上一篇博文 [從0開發3D引擎(十一):使用領域驅動設計,從最小3D程式中提煉引擎(第二部分)](https://www.cnblogs.com/chaogex/p/12411575.html) # 下一篇博文 [從0開發3D

領域驅動設計架構風格

des 設計 表達 對象 切入點 解決 基於 1.5 pattern 領域驅動設計 (DDD) 是面向對象的軟件設計方法,基於業務領域、元素和行為,以及它們之間的關系。其目標是將潛在業務領域的實現用業務領域專家語言定義的領域模型來表達出來。領域模型可以看一個框架,讓業務變得

EF Code first 和 DDD (領域驅動設計研究)系列一

發的 tex bsp cti 設計 ron 映射 developer devel 在上個公司工作時,開發公司產品的過程中,接觸到了EF Code first. 當時,整個產品的架構都是Lead developer設計建立的,自己也不是特別理解,就趕鴨子上架跟著一起開發了。

領域驅動設計(DDD)- 請先搞清楚一些概念

責任 可能 升級 是你 ora ext 計數 方法 避免 開發一個新系統   一般我們開始開發一個商業系統都需要做什麽?讀需求文檔去查找功能點,拆解任務。多數情況下,拆解項目是為了評估工作,做評估、分配任務到個人、設計數據庫結構,然後就開始了Coding。 所以,這種方

【DDD】領域驅動設計實踐 —— 架構風格及架構實例

讀取 bili 邏輯 stat orcal ransac 應用服務 業務場景 解讀 概述 DDD為復雜軟件的設計提供了指導思想,其將易發生變化的業務核心域放置在限定上下文中,在確保核心域一致性和內聚性的基礎上,DDD可以被多種語言和多種技術框架實現,具體的框架實現需要根據

【DDD】領域驅動設計實踐 —— 限界上下文識別

團隊協作 協作 tin 組織 領域 ges 承擔 產品 進行 本文從戰略層面街上DDD中關於限界上下文的相關知識,並以ECO系統為例子,介紹如何識別上下文。限界上下文(Bounded Context)定義了每個模型的應用範圍,在每個Bounded Context中確保領域模

.NET領域驅動設計—初嘗(原則、工具、過程、框架)

事物 只需要 pos eight 封裝 bili 建模 成就 一個 閱讀目錄: 1.原則 1.1.精簡聚合 1.2.分離用例與接口功能(設計模式的用武之地) 2.工具、框架、組件 3.過程 1】原則 原則對於任何一項技術實現來說都是至關重要的,在設計某一個系統功能的

領域驅動設計實踐 —— UI層實現

mcg ndk don xiv llc clu dji can vdc http://www.fjrcw.cn/zhiwei/company-1481.htmlhttp://2shou.guilinlife.com/product-386-816469.htmlhttp:/

領域驅動設計系列(2)淺析VO、DTO、DO、PO的概念、區別和用處

服務 完全 session 並且 main 解決 業務 導致 teacher   上一篇文章作為一個引子,說明了領域驅動設計的優勢,從本篇文章開始,筆者將會結合自己的實際經驗,談及領域驅動設計的應用。本篇文章主要討論一下我們經常會用到的一些對象:VO、DTO、DO和PO。

領域驅動設計:軟件核心復雜性應對之道》讀書筆記

風暴 基於模型 自動 知識 有效 嚴格 就是 專家 body 1.Eric Evans強調要聚焦於軟件的核心領域,以它來驅動開發。軟件能夠在市場上賣出去。是因為它封裝了別的軟件所滅有的一些核心領域知識,這就是核心競爭力,是利潤所在的地方,也是最值得下功夫的地方,再難也不能逃

領域驅動設計:軟件核心復雜性應對之道pdf

核心 案例 項目案例 ans weight line 作者 tle 方法 下載地址:網盤下載 內容簡介《領域驅動設計:軟件核心復雜性應對之道》是領域驅動設計方面的經典之作。全書圍繞著設計和開發實踐,結合若幹真實的項目案例,向讀者闡述如何在真實的軟件開發中應用領域驅動設計

領域驅動設計

代碼 包括 行為 data ech 不同的 好處 區別 權限 1.什麽是領域驅動設計(DDD:Domain Driven Design) 領域驅動設計(DDD)是一種基於模型驅動的軟件設計方式。它以領域為核心,分析領域中的問題,通過建立一個領域模型來有效的解決領域

領域驅動設計-分享

技術問題 詳細分析 上下文 mage oot class 頁面 val 約束 概述 領域驅動不是純粹的技術問題,領域建模(建立數據表只是一部分)是領域專家(客戶/產品團隊)和開發人員溝通努力、抽象的的結果。 領域建模的目的是,經過有效的溝通、詳細分析、 良好設計可以更好的適