前言正如領域驅(qū)動設計之父 Eric Evans 所著一書的書名所述,領域驅(qū)動設計(Domain Driven Design)是一種軟件核心復雜性應對之道。 在我們解決現(xiàn)實業(yè)務問題時,會面對非常復雜的業(yè)務邏輯。即使是同一個事物,在多個子業(yè)務單元下代表的意思也是不完全一樣的。比如「商品」這個詞,在商品詳情頁語境中,是指「商品基本信息」;在下單頁語境中,是指「購買項」;而在物流頁面語境中,又變成了「被運送的貨物」。 DDD 的核心思想就是讓正確的領域模型發(fā)揮作用。所謂「術業(yè)有專攻」,DDD 指導軟件開發(fā)人員將不同的子業(yè)務單元劃分為不同的子領域,在各個子領域內(nèi)部分別對事物進行建模,來應對業(yè)務的復雜性。
一、重構優(yōu)惠中心的背景我們在實際的開發(fā)過程中都遇到過這種情況,最初因為業(yè)務邏輯比較單一,為了快速實現(xiàn)功能, 以及對成本、風險等因素的綜合考慮,我們會為業(yè)務統(tǒng)一創(chuàng)建一個大的模型,各個模塊都使用這同一個模型。但隨著業(yè)務的發(fā)展,各子領域的邏輯越來越復雜,對這個大模型的修改就會變成一種災難,有時明明是要改一個 A 子領域的邏輯,卻莫名其妙影響到了 B 或者 C 子領域的線上功能。 優(yōu)惠中心就是一個例子。優(yōu)惠中心主要負責馬蜂窩各業(yè)務線商品的優(yōu)惠活動管理,以及計算不同用戶的優(yōu)惠結果。「商品管理」和「優(yōu)惠管理」作為兩個不同的業(yè)務單元,在初期被設計為共用一個商品模型,由商品模塊統(tǒng)一管理。 圖1 :初期商品模型
出現(xiàn)的問題隨著業(yè)務的發(fā)展,優(yōu)惠的形式不斷推陳出新,業(yè)務形態(tài)逐漸多樣,業(yè)務方的需求也越來越個性化,導致后期的優(yōu)惠中心無論從功能上還是系統(tǒng)上都出現(xiàn)了一些具體的問題: 1. 功能上來說,不夠靈活 優(yōu)惠信息是作為商品信息的一個屬性在商品管理模塊配置的。比如為了引導用戶使用 App 需要設置 A 類型優(yōu)惠,就通過在商品信息的編輯頁面增加一個 A 類型優(yōu)惠配置項實現(xiàn);如果某個商品的 A 類型優(yōu)惠需要在 0:00 分生效,業(yè)務同學就必須在電腦前等到 0:00 更新商品信息來上線優(yōu)惠活動。 另外,如果想要創(chuàng)建針對所有商品都適用的優(yōu)惠,按照之前的模式,所有的商品都要設置一遍,這幾乎是不可接受的。 2. 從系統(tǒng)層面看,不易擴展 優(yōu)惠信息存儲在商品信息中,優(yōu)惠信息是通過商品管理模塊的接口輸出的。如果要新增一種優(yōu)惠類型,商品信息相關的表就要增加字段,商品的表會越來越大;如果要迭代一個優(yōu)惠的邏輯,就有可能影響到商品管理模塊的功能。 3. 不利于迭代 由于優(yōu)惠信息僅僅作為商品的一個屬性,沒有自己的生命周期,所以很難去統(tǒng)計某一次設置的優(yōu)惠的投入產(chǎn)出比,從而指導后續(xù)的功能優(yōu)化。 重構優(yōu)惠中心的預期
二、為什么選擇 DDD避免貧血模型基于傳統(tǒng)的 MVC 架構開發(fā)功能的時候,Model 層本質(zhì)上是一個 DAO 層,業(yè)務邏輯通常會封裝在 Service 層,然后 Controller 通過調(diào)用 Service 層來完成對外的功能。這種模式下,數(shù)據(jù)和行為分別被割裂到了 Model 和 Service 兩層。我們把這種只承載數(shù)據(jù),但沒有業(yè)務行為的 Model 稱為「貧血模型」。 我們在和業(yè)務方了解需求的過程中,使用到的對象都是現(xiàn)實業(yè)務的映射,是行為和屬性的綜合體。需求確定好之后,我們開發(fā)的過程中,人為把行為和數(shù)據(jù)拆分成了兩部分,做了一次轉(zhuǎn)換。隨著需求的迭代,人員的更迭,開發(fā)看到的代碼和業(yè)務方的需求越來越對應不上,導致很多代碼誰也不知道對應的是什么業(yè)務邏輯,這種現(xiàn)象被稱為由貧血模型帶來的「失憶癥」,最終導致的是一個維護成本極高的大泥潭系統(tǒng)。 領域驅(qū)動設計的核心就是基于業(yè)務邏輯去建模,避免貧血模型,減少設計和開發(fā)過程中對業(yè)務信息的丟失和轉(zhuǎn)換。在業(yè)務邏輯迭代的過程中,系統(tǒng)通過調(diào)整對應的業(yè)務模型就可以完成迭代。
三、落地過程關鍵點:業(yè)務邏輯抽象要做到基于業(yè)務邏輯建模,就要合理地抽象。因為業(yè)務表象千差萬別,產(chǎn)品經(jīng)理和軟件設計人員需要和業(yè)務專家深入交流,并且從離散的信息中抽象出業(yè)務內(nèi)在的邏輯。 比如旅游業(yè)務售賣的商品和標品不同,有些優(yōu)惠是不考慮人群的,比如使用優(yōu)惠券,所有類型的庫存都可以享受;但如 N 人 N 折這類優(yōu)惠,成人價可以享受,兒童價和單房差就不可以。基于這個特點,我們對優(yōu)惠中心的商品模型做了抽象,抽象出來「是否可以參與件數(shù)計算」和 「是否可以參與價格計算」兩個通用屬性。這樣既實現(xiàn)了基于業(yè)務邏輯建模,又不會陷入業(yè)務邏輯千差萬別的表象中。 3.1 戰(zhàn)術設計第一步:統(tǒng)一語言,提煉關鍵詞準確的語言對于產(chǎn)品、運營、開發(fā)等各方對齊需求非常重要,我們需要將優(yōu)惠邏輯當中的概念抽象為各方都能理解的詞語,以達成共識。作為開發(fā)人員來說,對領域的理解一般來說是比較少的,為了抽象出合理的語言讓產(chǎn)品和業(yè)務方都能理解,就需要充分理解業(yè)務背景和需求。在熟悉業(yè)務和需求的過程中,提煉出若干關鍵字,這些關鍵詞就是最初產(chǎn)生的領域概念和通用語言。比如:
第二步:抽象領域模型根據(jù)單一職責的原則,一個領域概念對應一個領域?qū)ο?。領域?qū)ο笥?strong>實體和值對象之分:
區(qū)分實體和值對象對系統(tǒng)設計有很大意義,實體是我們需要重點關注和設計的,而值對象則只使用它的「值」就可以了。這樣可以簡化系統(tǒng)的復雜度,將精力聚焦在核心領域?qū)ο?。不難理解,優(yōu)惠活動毋庸置疑是一個實體,優(yōu)惠類型就是一個值對象。 但也存在某些業(yè)務行為是不能歸于某個實體或值對象的,可以將它們歸為領域服務:
有一些領域邏輯,比如「優(yōu)惠排序」和「優(yōu)惠互斥」,他們涉及到多個優(yōu)惠類型,也就是多個領域?qū)ο?。如果也被設計為領域?qū)ο?,就打破了單一職責的原則,所以我們把這部分跨多個領域?qū)ο蟮臉I(yè)務邏輯放到「領域服務」層。 第三步:抽象領域?qū)ο笾g的關聯(lián)關系將相關聯(lián)的領域?qū)ο筮M行顯式分組,來表達整體的概念(也可以是單一的領域?qū)ο螅簿褪?strong>「聚合」。 比如優(yōu)惠活動是優(yōu)惠類型、優(yōu)惠范圍等的聚合;優(yōu)惠類型是優(yōu)惠規(guī)則和優(yōu)惠方案的聚合;優(yōu)惠規(guī)則是限制維度的聚合;優(yōu)惠方案是優(yōu)惠手段的聚合: 圖2 :關聯(lián)關系示意 聚合的主要功能是把領域?qū)ο蠓纸M,外部的唯一訪問點就是聚合根,這樣可以避免處理領域?qū)ο箝g的一一對應關系,只需要處理聚合和聚合之間的關系就行了。 第四步:走查場景,調(diào)整領域模型領域模型的調(diào)整是貫穿整個設計和開發(fā)過程的,隨著業(yè)務的調(diào)整,領域模型也需要調(diào)整。比如優(yōu)惠中心后期引入了會員卡的優(yōu)惠類型,那么就需要把優(yōu)惠券這個優(yōu)惠類型的顯示,調(diào)整為與會員卡互斥的優(yōu)惠券和與會員卡不互斥的兩種。 第五步:簡化設計,降低系統(tǒng)復雜度建模的本質(zhì)是對現(xiàn)實事物的一種簡化和抽象,指導我們忽略和問題域無關的事實,提取和問題域息息相關的信息。以優(yōu)惠中心為例,最初的方案里我們設計了優(yōu)惠類型管理的功能,根據(jù)不同的優(yōu)惠規(guī)則和優(yōu)惠方案自動組合成不同類型的優(yōu)惠類型。但是可以預見,未來的優(yōu)惠類型是有限的,并且每個優(yōu)惠類型都有會自己的特殊配置,比如 N 人優(yōu)惠里的 每 N 人/第 N 人;早鳥中的提前 N 天等。也就是說,根據(jù)優(yōu)惠規(guī)則和優(yōu)惠方案自動生成優(yōu)惠類型基本是沒有使用場景的,因此也就去掉了這個設計。 再如,對優(yōu)惠的限制我們最初是設計在優(yōu)惠活動維度,經(jīng)過權衡,為了降低系統(tǒng)復雜度,最后實現(xiàn)在了優(yōu)惠類型層面。以「蜂搶」優(yōu)惠類型為例,它的規(guī)則是所有的蜂搶活動都是 1 個用戶只能搶一次,沒有必要把這個限制放在優(yōu)惠活動維度,在優(yōu)惠類型層面控制就可以了。 3.2 戰(zhàn)略設計戰(zhàn)略設計處理的是不同限界上下文之間的拆分和集成邏輯。限界上下文比較抽象,結合我們在文章開始提到的不同語境中的「商品」例子來理解,同一個詞如果不說明白所處的語境,是無法準確描述清楚其表達的含義的?!刚Z境」其實就是「上下文」,對應不同「子領域」。同理,如果不在一個限定好的上下文中去設計領域模型,設計出的領域模型是不清晰的,它就會同時支持多個上下文。 這里需要說明一點,如果是從零搭建一個全新的電商系統(tǒng),首先需要做的應該是戰(zhàn)略設計。而優(yōu)惠中心是建立在現(xiàn)有大的電商系統(tǒng)基礎上,相當于作為其中一個子領域進行重構,所以我們才會先來做戰(zhàn)術設計,再考慮在完整的電商系統(tǒng)下它與外部其他環(huán)境之間的關系,也就是戰(zhàn)略設計。 優(yōu)惠中心內(nèi)部場景區(qū)分優(yōu)惠中心包括了服務于 B 端用戶的優(yōu)惠活動管理和服務于 C 端用戶的優(yōu)惠計算這兩個不同的子業(yè)務單元:
圖3 :優(yōu)惠中心內(nèi)部場景區(qū)分
優(yōu)惠中心與外部系統(tǒng)集成在整個電商系統(tǒng)的環(huán)境下,優(yōu)惠中心作為一個子域,處于自己的限界上下文當中。使用優(yōu)惠中心服務的詳情頁、下單頁都處于自己各自的限界上下文,所以調(diào)用優(yōu)惠中心的時候就需要設計它們之間的上下文映射方式。 調(diào)用和被調(diào)用方使用的戰(zhàn)略設計方法通常有以下幾種:
結合我們的實際情況來看,調(diào)用優(yōu)惠中心的可能會是不同團隊的開發(fā)人員,而優(yōu)惠中心又不想被不同的上游侵入內(nèi)部設計中,所以「客戶方-供應方」和「遵奉者」模型都不適合;另外優(yōu)惠中心前期接入方會比較少,而且會不斷迭代,使用「開放主機服務」也不太合適。綜合考慮下,防腐層的設計比較適合優(yōu)惠中心。 下圖是優(yōu)惠中心的業(yè)務架構示意,中間的應用服務層采用的就是防腐層的設計,反映優(yōu)惠中心與外部系統(tǒng)集成時的上下文映射關系:
圖4 :優(yōu)惠中心業(yè)務架構 3.3 架構實現(xiàn)優(yōu)惠中心選擇的是經(jīng)典的分層架構。從上到下為用戶接口層、應用服務層、領域?qū)雍蛡}儲層。圖中不同的顏色塊分別對映外部服務、應用服務、領域服務、聚合根、實體、值對象和倉儲。
圖5 :優(yōu)惠中心分層領域模型
四、問題及近期規(guī)劃1. 價格層優(yōu)惠現(xiàn)在公司面沒有一個統(tǒng)一的商品中心,并且各業(yè)務線對商品的定義差別很大。比如自由行的商品包括出行日期、價格類別(成人價、兒童價)和套餐類別等層級;而火車票的商品包含座次、席別、目的地和出發(fā)地等層級。 如果優(yōu)惠中心抽象出一種通用的商品層級來適配各個業(yè)務線,那實際上就是優(yōu)惠中心要對商品進行標準定義,但是這個標準與后續(xù)商品中心的標準定義很有可能是不一致的,如果不一致優(yōu)惠中心就要做大的改版。所以最終的解決方案可能還要通過推進統(tǒng)一商品中心的建立來解決。 2. 性能問題領域驅(qū)動設計帶來的弊端就是類的增多。目前優(yōu)惠中心的技術?;?PHP, PHP 是一種解釋型語言,在DDD 模式下即使有了 OPCode 等緩存技術,執(zhí)行階段的耗時相對其他靜態(tài)數(shù)據(jù)類型的語言還是較大。所以后面計劃將優(yōu)惠中心使用 Java 技術棧重構,來進行性能上的優(yōu)化。
五、小結本文介紹了馬蜂窩電商優(yōu)惠中心基于 DDD 進行重構的一些實踐經(jīng)驗。DDD 的思想也幫助我們在業(yè)務迭代的過程中將架構設計得更加合理。 當然,是否采用業(yè)務驅(qū)動設計的思想,需要取決于業(yè)務和團隊的實際情況。在馬蜂窩業(yè)務的快速發(fā)展下,我們在架構設計上還將做更多的探索,也將持續(xù)與大家交流。 本文作者:徐興旺,馬蜂窩電商研發(fā)平臺服務團隊技術專家。 (馬蜂窩技術原創(chuàng)內(nèi)容,轉(zhuǎn)載務必注明出處保存文末二維碼圖片,謝謝配合。)
|
|