一、項目背景
教育領域,完整的流程板塊包括:招生拓客、線索管理、教務管理、學員管理、互動督學、口碑傳播。首先,在招生拓客環節,會通過線上營銷工具或線下地推方式收集潛在的學員線索信息,並錄入到線索管理系統中。在線索管理環節,會採用線索資源管理系統對收集的線索做統一管理,並將潛在學員轉爲真正的學員,提供給後續的教務管理使用。可見,線索管理在整個教育領域承擔着承上啓下的作用,重要性不言而喻。
此項目業務場景總覽見圖1-1,整個項目分爲兩大業務域,分別是線索域、配置中心域。其中,線索域主要負責線索收集、線索管理等功能;配置中心域負責管理一些公共配置資源,比如,線索關聯的標籤、來源等。
二、領域驅動基礎概念介紹
在介紹DDD相關基礎概念前,我先說明下爲什麼要使用DDD?在非DDD設計思路下的項目,我們一般先根據需求做數據庫表的設計,然後根據表結構設計推導出相應的實體對象,這樣的實體對象是數據模型轉換的結果。此時,這些對象只是數據的載體,是沒有行爲的。在這種設計模式下,業務流程實現上仍舊是面向過程式,是一種以數據爲中心的過程式思想,其開發過程可以理解爲是對數據移動、處理和實現的過程。而如果採用DDD的思想去設計,我們將建立一個基於面向對象設計的系統。接下來,我先介紹DDD的標準分層架構,然後介紹下需求分析階段非常有用的四色原型分析模式,最後簡要介紹下方案設計階段常用到的幾個DDD領域概念。
2.1 領域驅動設計標準分層架構
當前,業界比較通用的DDD架構採用的是四層模型,從下到上依次爲基礎設施層、領域層、應用層和用戶界面層。具體的分層架構見圖2-1。
2.1.1 基礎設施層
基礎設施層主要爲其他層提供通用的技術能力,比如,應用的消息發送、領域持久化等。在實際的項目應用中,本層主要用於持久化數據的讀取和寫入,我們可以在這一層,將需要持久化的領域對象序列化到指定的存儲介質中,比如:數據庫、Hbase、MongoDB、ES等,同理,亦可從這些存儲介質中取出數據並組裝成領域對象。在這一層,一般會採用倉儲機制實現領域持久化能力。
2.1.2 領域層
領域層,亦稱模型層,屬於業務系統中最核心的一層,整個系統中幾乎所有的業務邏輯均會在該層實現。本層主要包含領域模型和領域服務。
(1)領域模型
領域模型用來抽象複雜的業務邏輯,將其轉換爲便於理解的概念圖模型,一般由實體和值對象構成。它與數據模型的不同點在於:數據模型描述的是對象的持久化方式,而領域模型表述的是領域中各個類,以及各類之間的關係。
(2)領域服務
領域服務可以認爲是領域模型的一種補充,因爲在實際建模過程中,一些概念本質上是一些操作,它們會涉及到多個領域對象,並且需要協調這些領域對象來完成這個操作,如果強行將這個操作歸類到某個對象,那麼這個對象就會承擔一些本不屬於它的職責,進而出現對象職責不明確的現象,此時,就需要領域服務來承載這些操作,用來串聯多個領域對象。比如,在線索管理項目中,線索詳情頁信息是由“線索基礎信息”、“標籤信息”、“來源信息”、“線索處理日誌信息”等構成的,我們在建模時,考慮到合理性將四者定義爲了四個單獨的實體,此時就出現了問題,線索詳情獲取時,完整的線索信息怎麼聚合,該放在哪裏?考慮到四者信息領域相關性,我們便引入了領域服務,用來承載線索信息聚合操作。
2.1.3 應用層
應用層用來提供應用服務,主要負責業務用例的編排和組裝,它和領域服務的主要區別,個人觀點是:是否處理業務邏輯。具體介紹的話,就是協調領域層與用戶界面層之間的關係,對外爲用戶界面層提供各種應用功能,對內調用領域層的領域對象或領域服務完成各種業務編排、組裝。
2.1.4 用戶界面層
用戶界面層主要負責用戶信息的展示,具體來說就是:請求應用層獲取用戶所需要展現的數據,以及發送命令給應用層要求其執行某個用戶命令。在實際應用中,本層是可以不存在的,比如,在教育團隊早期的項目中,前端是通過http的方式調用後端服務的,我們在這一層通過提供REST服務的方式與前端交互,之後,統一採用了RPC調用的方式,便弱化了這一層:在該層聲明二方服務接口與前端node層交互,然後在應用層作具體的接口實現。
2.1.5 線索管理應用工程結構簡單介紹
本小節會先給出線索管理項目涉及應用的工程目錄結構,然後結合DDD的四層架構做對比分析。首先,我們來看下工程目錄結構,見圖2-2。
出於商業保密性,實際工程結構中部分模塊做了隱藏
目錄中各模塊的定義如下:
- demo-api:接口層,系統之間或者對外的接口聲明,通過RPC調用的方式對外提供二方服務。
- demo-biz:應用服務、領域服務處理層,接口層所聲明接口的具體實現。
- demo-dependency:外部系統的調用封裝,比如,系統需要調用商品中心的服務,則需要在本module中封裝client。
- demo-domain:領域層,系統領域的一些model、上下文對象、倉儲接口定義等。
- demo-web:對外的REST接口。
- demo-dal:基礎設施層,數據持久化。
與DDD四層構架的對應關係見下表。
用戶界面層 | demo-web |
demo-api | |
應用層 | demo-biz(api) |
領域層 | demo-biz(domain) |
demo-domain | |
demo-dependency | |
基礎設施層 | demo-dal |
2.2 需求分析利器 — 四色原型圖
簡單的業務需求,一般使用用例圖就可以表述清楚了,如果業務再複雜一些,我們可以附加一些時序圖、狀態圖等加以說明,但是當業務非常複雜時,怎麼去尋找業務中的關鍵點以及各個點之間的聯繫呢?或者有沒有一個比較科學的理論,指引我們去分析呢?我們可以考慮使用四色原型分析模式。它主要用於業務分析階段,用來分析業務行爲、參與對象、業務對象關係等。那什麼是四色原型圖呢?我們先來看下它的四個構成元素,具體如下:
(1)時刻-時間段原型(Moment-Interval Archetype)
原型簡稱MI,表述的是某刻或某段時間內發生的一件事,比如:租房合同簽署,是在某個時刻簽署的,它有發生日期、行爲人;租房行爲是在一段時間內發生的,它有開始、結束時間和退租行爲。這些我們都是可以通過此原型來表達的。在畫原型圖時,採用粉紅色表示。
(2)參與方-地點-物品原型(Part-Place-Thing Archetype)
原型簡稱PPT,用來表示參與某個活動的人或物,地點則是活動的發生地。比如簽署租房合同這個行爲,合同、承租人分別對應這裏的物、人,中介辦公室對應這裏的地點。在畫原型圖時,使用綠色表示。
(3)描述原型(Description Archetype)
原型簡稱DESC,是對PPT公共屬性的描述,拿“簽署租房合同”這個場景爲例,在合同中會有一些租期、租金、押金、違約條件等約定,這些約定信息便可採用DESC原型來描述。繪製原型圖時,採用藍色表示。
(4)角色原型(Role Archetype)
原型簡稱Role,這裏的角色,就是我們平時所理解的“身份”。以“簽署租房合同”這個場景爲例,簽署行爲人有承租人和中介工作人員,這裏的角色便是指“承租人”和“中介工作人員”。繪製原型圖時,採用黃色表示。
總結:如果必須要用一句話來概括四色原型的話,那就是:一個什麼樣的人或物以某種角色在某個時刻或某段時間內在某個地點參與某個活動。 其中“什麼樣的”就是DESC,“人或物”、“地點”就是PPT,“角色”就是Role,而”某個時刻或某段時間內的某個活動"就是MI。
2.3 DDD幾個核心領域概念
2.3.1 實體
實體是一個具有身份和連貫性的概念,它具有以下幾個特徵:
- 實體是數據(屬性)和行爲(業務邏輯關係)的結合體;
- 每個實體都有自己的唯一標識,判斷兩個實體對象是否相等,是通過唯一標識來判斷的。比如,兩個實體對象,如果唯一標識相等,即使其他屬性不相等,這兩個實體也會認爲是同一個。實體的其他屬性不相等,表徵的是同一個實體在其生命週期的不同階段。
- 實體的唯一標識屬性值是不可變的,其他屬性值是可變的。
舉個例子簡單說明下,比如在有贊精選內容平臺(類似於小紅書的電商導購平臺)這個業務域中,每一篇“博文”就是一個業務實體,可以採用“博文id”作爲實體的唯一標識,然後這個博文實體擁有着屬性(標題、作者、發表時間、內容等)和行爲(更新博文、刪除博文、關聯導購商品等),同時,屬性是會隨着行爲而不斷變化的。
2.3.2 值對象
值對象一般會作爲一個屬性存放於一個實體內部,它具有以下幾個特徵:
- 值對象不需要唯一標識,判斷兩個值對象是否相等,是通過值對象內部所有屬性值是否相等來判斷的。
- 值對象的屬性值是不允許變化的,即值對象的實體在創建之後就不會變了,如果要改變其屬性值,就需要先把此對象刪除,然後重新創建一個新對象。
同樣以“有贊精選內容平臺”爲例說明下,用戶可以針對博文發起留言,同時,我們會精選出一些留言置頂,對於“置頂的留言”我們可以定義爲值對象,並將其作爲一個屬性放置於博文實體中,一旦置頂留言發生變化,我們只需要將新的置頂留言重建爲值對象,並賦值給博文實體的這個屬性即可。
2.3.3 聚合
聚合是一組具有內聚關係的領域對象(包括實體和值對象)的集合,這裏的一組可以是一個或多個實體。每個聚合都會有一個根實體(亦稱聚合根),它主要用來和外界交互,即外部對象如果想訪問聚合內的實體,必須先訪問聚合根,然後聚合根再和內部要訪問的實體進行交互。
還是拿“有贊精選內容平臺”舉例說明,一篇博文中,它包含博文基礎信息(內容、標題等)、關聯的商品信息、關聯的標籤信息等,這一組合就是一個聚合,其中,“博文基礎信息”可以設置爲這個組合的聚合根。
2.3.4 倉儲
首先說明下倉儲被設計出來的初衷,在領域模型中,對象被創建出來後一般會在內存中活動,待其不活動了後,需要將其進行持久化存儲。然後,當我們需要重建對象時,需要根據對象當前狀態進行重建。可見這整個過程中,會頻繁的與數據庫(廣義的數據庫,包括關係型數據庫、NoSql數據庫等)打交道,進行對象的創建、組裝等。因而,能否提供一種機制,幫助我們管理領域對象以及做對象持久化,倉儲並應運而生了。
倉儲,又稱資源庫,它具有以下幾個特徵:
- 倉儲是連接領域層和基礎設施層的橋樑,一般將倉儲接口定義放在領域層,倉儲的具體實現放在基礎設施層。這樣做的好處是:解耦了領域層與ORM之間的聯繫,任何ORM相關的變更,只需要修改倉儲的實現便可,對於領域層倉儲接口的定義一般是不需要做修改的。
- 倉儲裏面存儲的對象一定是聚合,因爲領域模型中都是以聚合來劃分業務邊界的,所以在實際應用中,我們只會對聚合設計倉儲。同理,我們在倉儲中做數據更新、刪除等操作時,應該以聚合爲單位進行操作,而不是僅操作聚合中的某一個實體。
三、線索資源管理DDD實戰
結合四色原型圖,設計領域模型的步驟可概括爲以下幾步:
- 根據需求,採用四色原型分析法建立一個初步的領域模型;
- 進一步分析領域模型,識別出哪些是實體,哪些是值對象,哪些是領域服務;
- 對實體、值對象進行關聯和聚合,提煉出聚合邊界和聚合根;
- 爲聚合根設計倉儲(一般情況下,一個聚合分配一個倉儲),同時,思考實體、值對象的創建方式,是通過工廠創建,還是直接通過構造函數;
- 走查需求場景,驗證設計的領域模型的合理性。
在圖1-1(線索管理業務總覽圖)中可以看出,“線索域”是其核心部分,接下來,我主要針對“線索域”,按上述步驟一點一點推導出其領域模型。
3.1 場景分析提煉四色原型圖
在線索收集部分,不管是採用線上營銷工具渠道,還是線下地推渠道,最終觸發的業務場景都是線索新增;在線索管理部分,業務場景主要是:新增/更新線索、查詢線索、分配線索、跟蹤線索、放棄線索等。業務場景用例可見圖3-1。
(1)新增/更新線索四色原型圖
業務規定,只有高級管理員、課程顧問、普通管理員等有操作線索的權限,按照四色原型一句話理念“一個什麼樣的人或物以某種角色在某個時刻或某段時間內在某個地點參與某個活動”,可推導出操作的參與方原型是商家,參與方角色有商家高級管理員、課程顧問、普通管理員等;物品原型是線索,其中線索由線索基礎信息、線索標籤、線索來源三者構成;參與的活動是“新增/更新線索”。提煉出的四色原型圖見圖3-2。
(2)查詢線索
參與方原型是商家,參與方角色有商家高級管理員、課程顧問、普通管理員等;物品原型是線索基礎信息、線索標籤、線索來源;參與的活動是“查詢線索”。提煉出的四色原型圖見圖3-3。
(3)分配線索
每個線索如果需要人跟進的話,必須給線索分配一個跟進人。如果當前跟進人無法完成此線索跟蹤,其可以將此線索轉讓給其他人(這裏的線索分配者、線索承接人、線索原跟進人的身份均是“高級管理員、課程顧問、普通管理員”之一)。從上述場景可以得出:參與方原型是商家,參與方角色有商家高級管理員、課程顧問、普通管理員等;物品原型是線索基礎信息、線索標籤、線索來源;參與的活動是“分配線索”。提煉出的四色原型圖見圖3-4。
(4)跟蹤線索
課程顧問拿到分配過來的線索後,就需要去跟蹤了,在跟蹤期間,課程顧問可以記錄相應的跟蹤記錄。此場景中,參與方原型是商家,參與方角色是課程顧問;物品原型是跟蹤記錄;參與的活動是“添加跟蹤記錄”。歸納出的四色原型見圖3-5。
(5)放棄線索
如果課程顧問覺得當前線索比較難跟進,可以選擇放棄此線索。從上述場景可以得出:參與方原型是商家,參與方角色是課程顧問;物品原型是線索(基礎信息、標籤、來源);參與的活動是“放棄線索”。提煉出的四色原型圖見圖3-6。
綜合以上所有場景,可得出圖3-7所示的“線索域”四色原型圖。
3.2 領域模型中實體/值對象/領域服務/聚合識別
一般來說,可以將四色原型圖中的原型和DDD做簡單的映射,比如:PPT原型描述的是某個活動下的唯一個體,其可對應到DDD中的實體;Role原型表述的是實體在不同狀態下的表現,一般將其放置於實體中,一起構成一個完整的帶狀態實體;DESC原型描述的是PPT的公共屬性,一般作爲值對象存儲;MI原型描述的是某個活動,可間接對應領域服務。
我們回過來看下圖3-7,在圖中,有“商家、線索基礎信息、跟蹤記錄、來源信息、標籤信息”5個PPT,我們可以據此定義5個實體,“高級管理員”、“課程顧問”、“普通管理員”可以認爲是商家在不同身份下的表現,可在商家對象中使用一個標識符來描述。於是,我們可以總結出以下實體,見圖3-8。
實際的線索信息比圖3-8中定義的要複雜,出於商業保密性,這裏僅列出部分字段,且部分字段採用xxx來表示。
接着,我們進一步分析實體間的關係,提煉出聚合邊界和聚合根,並定義出倉儲。
在線索域中,線索是核心,很明顯ClueEntity與SourceEntity、RecordEntity、TagEntity、UserEntity是相關聯的,而後四者間是沒有聯繫的。首先,來看下ClueEntity和UserEntity,線索在創建之初是可以沒有跟進人(用戶)的,但在之後被跟進的過程中,需要強制綁定一個跟進人(用戶),而用戶脫離線索是不具有存在價值的。同時,本項目中,用戶信息僅作爲線索的歸屬屬性存在,最終我們將UserEntity(改名爲UserVO)作爲值對象放置於ClueEntity內,且令線索信息實體爲聚合根;然後,分析下ClueEntity和SourceEntity、TagEntity、RecordEntity,主要從兩個方面考慮是否需要組成聚合:
(1)聚合代表的是一個完整的概念,具有內部一致性,即聚合內的對象要麼一起獲取,要麼一起更新,要麼一起刪除。假如聚合在被保存時,內部任意一個對象被修改了,都需視爲聚合被修改了,此時應令保存失敗。所以,在定義聚合時,在保證合理性的情況下,儘量設計小的聚合。在線索管理中,線索管理人員會頻繁給線索關聯標籤、來源、跟進記錄等信息,從內部一致性角度考慮,三者分開可能會更好。
(2)聚合內聚合根和對象間要保持不變性。何爲不變性?簡單來說,對象之間存在某種不變的規則。舉個例子說明下,x=y+5,如果規定y大於1,那麼x一定大於6。回到線索管理,ClueEntity和SourceEntity、TagEntity、RecordEntity間並不存在這種不變性,因爲任意一個來源、標籤、跟蹤記錄一定有一條對應的線索,但一條線索可以沒有來源、標籤、動態記錄,同時,來源、標籤、跟蹤記錄均可以在各自領域被單獨訪問到。
結合以上兩點,這裏我採用的策略是:SourceEntity、TagEntity、RecordEntity三者各自定義爲一個聚合,本身作爲聚合根。那問題也來了,在查詢線索詳情時,線索是包含來源、標籤、跟蹤記錄信息的,但是ClueEntity聚合內又不包含三者,此時該怎麼解決信息聚合呢?我們採用了領域服務,來做領域對象間的聚合。
最後,定義下倉儲,這裏我採用一個聚合對應一個倉儲的原則來定義的。最終,我們得到下表所示的領域模型。
倉儲 | 聚合 | 聚合根 |
ClueRepository | ClueAggregate | ClueEntity, UserVO |
SourceRepository | SourceAggregate | SourceEntity |
TagRepository | TagAggregate | TagEntity |
RecordRepository | RecordAggregate | RecordEntity |
對應的類圖可見圖3-9。
四、總結與思考
本文主要從“線索資源管理”這一實際項目出發,詳細的闡述了從需求分析到方案設計階段,如何採用DDD思想一步一步提煉領域模型。首先,在第一章中重點介紹了項目的背景,以及它在教育域中的價值,同時給出了其主要的業務場景,方便大家對本項目有一個整體的印象。然後,在第二章中詳細的介紹了DDD的分層結構,在需求階段可採用的利器 — 四色原型分析法,以及方案設計階段需要使用的幾個DDD領域概念。最後,在第三章中,結合第一、二章的項目背景、領域概念,一步一步提煉出了本次項目的領域模型。鑑於作者經驗有限,我對領域驅動的理解難免會有不足之處,歡迎大家共同探討,共同提高。
本文轉載自公衆號有贊coder(ID:youzan_coder)。
原文鏈接: