領域驅動設計 -DDD- 之實踐
- 簡介
領域驅動設計是一個應對複雜應用系統的設計方法,它通過一系列從粗到細粒度的邏輯邊界劃分,從而創建系列的高內聚的領域模型,並使用與領域模型一致性的代碼實現。最終,高複雜度的應用系統被劃分爲一個個小的低複雜度服務 / 功能 / 任務。後續文章不按照常見的戰略設計 + 戰術設計實現,只按照自己的理解來展開。
- 基本概念
領域驅動設計核心是利用業務概念創建領域模型對象最終完成系統設計,而不是數據存儲出發設計系統。業務概念的來源主要是用戶故事中各種業務術語。下述概念的粒度由粗到細,例如一個上下文邊界中包含一個及以上的應用服務。
2.1 上下文邊界
在有界上下文中,領域對象纔會具有確定的語義,保證沒有二義性。劃分好有界上下文,就可以知道領域對象應該放在哪個上下文中實現。實際上,微服務的拆分和設計是基於有界上下文,一個上下文對應一個微服務,但是防止過度拆分帶來的維護成本激增,往往會將多個上下文邊界作爲一個服務管理。例如,在電商領域,一個 “商品” 在不同的上下文中可能有不同的含義和屬性。在銷售上下文中,它可能包括價格、促銷信息等屬性;而在庫存管理的上下文中,它可能包含庫存數量、存儲位置等屬性。通過定義這兩個不同的上下文邊界,我們可以清晰地區分 “銷售商品” 和“庫存商品”,避免因概念混淆而導致的業務邏輯錯誤。
2.1.1 應用服務
應用服務通常以業務用例爲粒度,每個服務對應一個獨立的業務用例。應用服務主要負責對服務內部的領域服務編排。 假設我們有一個電子商務平臺,該平臺有一個功能是 “用戶提交訂單”。這個業務用例可以由一個名爲OrderSubmissionService
的應用服務來實現。這個服務會處理用戶提交訂單的整個流程,包括驗證用戶輸入的數據、創建訂單、計算訂單總額、檢查庫存、記錄交易日誌等。在這個過程中,OrderSubmissionService
會調用領域層中的多個聚合根和實體來完成這些任務,例如調用Order
聚合根的placeOrder
方法,以及Product
實體的decrementStock
方法。
2.1.1.1 聚合
聚合由一個聚合根、多個實體、多個領域服務、多個領域事件以及一個倉儲實現,這些對象在業務上高度關聯,並且作爲一個整體被統一管理,具有強一致性。聚合內實現高內聚的業務邏輯,如果特別複雜,則它的代碼可以獨立拆分爲微服務。
聚合根
聚合根也是一個實體,但是封裝了所在聚合內的所有領域對象的管理,並維護聚合內的強一致性。 聚合軟件包的根目錄,可以根據實際項目的聚合名稱命名,比如權限聚合。在聚合內定義聚合根、實體和值對象以及領域服務之間的關係和邊界。
領域事件
領域內產生的事件。可以關注用戶故事中,“當..., 則要...." 這種描述。
領域服務
負責編排聚合內實體和值對象,組合出一段業務邏輯,一個聚合可以只有一個領域服務類,如果比較複雜再考慮拆分。
倉儲
一個聚合對應一個倉儲,負責聚合的查詢和持久化。
實體
實體是最底層的領域對象之一,主要特徵是:
-
具有唯一標識符,各種屬性變更後標識符不變
-
包含屬性和方法,使用充血模型
值對象
值對象也是最底層的領域對象之一,主要特徵是:
-
不可變
-
通過對象屬性值來區分而不是標識符
- 工程實現(or 分層架構)
服務內部代碼的組織使用分層架構來組織,主要分爲用戶接入層、應用層、領域層、基礎設施層來作爲上述領域對象的載體。
3.1 用戶接入層
負責將用戶輸入轉換爲領域對象,並調用應用服務產生結果,將結果轉換爲展現數據。
dto
存放 web 接口 / soa 接口的入參和出參
assembler
存放 dto 與領域對象的轉換邏輯
facade
存放應用服務的編排代碼
3.2 應用層
event
負責事件的發佈和訂閱
publish
subscribe
service
負責存放應用服務的業務,使用 xxxCommand\xxxQuery\xxxEvent 明確意圖。
external
負責存放外部服務的接口
3.3 領域層
service
存放領域服務的邏輯,調用
entity
存放實體
valueObject
存放值對象
event
存放領域事件
倉儲
存放倉儲接口
3.4 基礎設施層
負責存放 rpc 調用、mp 配置以及事件訂閱 / 發佈接口的實現、倉儲接口的數據庫的實現以及緩存的實現等。
4 實踐
背景
用例 1:在一個設計工具中,家裝設計師可以打開方案,在方案中選中想要生成檯面的櫃子,然後選擇需要生成的檯面材質樣式,然後點擊一鍵生成檯面。同時,檯面材質和前後擋水樣式之間的約束關係有單獨的配置。生成的檯面塊要能夠恰好覆蓋櫃子表面。檯面塊和牆和櫃子重疊的部分會生成後擋水,其餘邊生成前擋水。如果用戶要求前擋水要內含,則櫃子表面的輪廓要包含前擋水;如果是外擴,則前擋水可以在櫃子外部。多個檯面如果相鄰等高,則需要合併一個檯面。檯面塊是平面板件模型,而前後擋水則是掃掠模型。櫃子和牆形成的閉合縫隙,如果面積小於 100 平米釐米則需要擴展臺面,補上縫隙。
用例 2: 生成好的檯面可以根據用戶的要求切割成多個檯面塊和多段前擋水和後擋水。
用例 3: 用戶選中方案中的櫃子,進入腳線生成環境,選擇腳線輪廓和材質樣式以及腳線高度,可以使用設計工具一鍵生成腳線,腳線是掃掠模型。同時,首尾相接的多段腳線可以合併爲一段。
任務
-
提取領域模型,並劃分上下文,並再用上下文重新組織領域模型
-
完成代碼的設計和組織
行動 && 結果
-
提取各種業務名詞、行爲還有事件 業務術語: 檯面、檯面塊、前擋水、後擋水、內含外擴、戶型、方案、牆、檯面合併、檯面補縫、檯面切割、腳線、櫃子、櫃子上表面、櫃子下表面、掃掠模型、放樣模型、材質、輪廓樣式。
-
劃分上下文
-
櫃子模型、掃掠模型、放樣模型
-
方案
-
戶型
-
櫃子、櫃子上表面、櫃子下表面
-
腳線、腳線高度、腳線材質、腳線樣式、腳線掃掠
-
檯面、檯面塊、前擋水、後擋水、檯面厚度、內含外擴、檯面合併、檯面補縫、檯面切割、前擋水樣式、檯面材質、前擋水材質、後擋水材質、後擋水樣式、前擋水掃掠、後擋水掃掠、檯面塊板件
-
檯面上下文邊界 (核心)
-
腳線上下文邊界 (核心)
-
櫃子上下文邊界(支撐)
-
戶型上下文邊界(通用)
-
方案上下文邊界(通用)
-
建模上下文邊界(通用)
-
提取隱藏概念以及上下文
對上述的部分行爲詳細展開,比如檯面合併:檯面合併是指兩個檯面材質、樣式一致、厚度一致且幾何圖形做交集,因此還有幾何的上下文,包含常用的 3 維長方形、3 維長方體、3 維多邊形、特殊的柱體(三維多邊形沿着面的法向量拉伸形成)以及相應的相交 / 差 / 並集操作等。
同時,考慮到維護性,兩個核心上下文分別使用單獨的服務作爲載體會導致維護成本比較高,尤其是腳線業務邏輯簡單,因此考慮放在使用同一個服務作爲載體。同時作爲支撐腳線和檯面的櫃子、掃掠、板件則作爲原子領域概念,組合構建出更高層的領域對象。 -
識別領域對象並分層
-
基礎設施層
-
數據配置 + mapper.xml 以及 mapper 以及接口實現
-
redis 配置 + caffeine 緩存實現
-
rpc client 調用以及 dto 與 do 轉換以及限流和熔斷機制
-
動態配置實現
-
線程池實現
-
代碼結構 (部分)
目錄: -
mq
-
storage
-
rpc
-
rocketMQ
-
數據庫 A
-
數據庫 B
-
config (數據源配置以及事務管理器配置)
-
po
-
assembler
-
mysql
-
redis
-
dto
-
assembler
-
facade(包含 rpc 的各種調用,渲染、模型以及戶型服務的實際調用)
-
util
-
entity
-
valueObject
-
Cabinet
-
Room
-
Sweep
-
Plank3d
-
Polygon3d
-
Curve3d
-
countertop
-
bottomMolding
-
CountertopRuleRepo
-
Countertop
-
CountertopBlock
-
CountertopDomainService
-
service
-
entity
-
repo
-
valueObject
-
event(空的)
-
exception
-
switch (業務開關,接口與實現分離)
-
service
-
event (沒用到可刪除)
-
ModelAppService
-
RenderAppService
-
RoomAppService
-
CountertopAppService
-
BottomMoldingAppService
-
extern
-
subscribet
-
publish
-
interface
-
controller
-
facade
-
dto
-
assembler
-
CountertopController
-
BottomMoldingController
-
web 層
-
application (依賴領域層)
-
domain
-
domain-primitive
-
infras(依賴領域層的接口)
基礎設施層部分代碼展示:
@Repository
public class DrawerClearanceConfigRepoImpl implements DrawerClearanceConfigRepo {
@Resource
private DrawerClearanceConfigMapper drawerClearanceConfigMapper;
@Resource
private DrawerClearanceConfigRelationMapper drawerClearanceConfigRelationMapper;
/**
* 通過抽屜ID列表查找配置。
*
* @param drawerId 抽屜的唯一標識ID
* @return 一個映射,將抽屜ID映射到它們的配置
*/
@Override
@Cacheable(value = "drawerClearanceConfigs", key = "#rootAccountId + '-' + #drawerId")
public DrawerClearanceConfig findConfigsByDrawerId(Long drawerId, long rootAccountId) {
final List<DrawerClearanceConfigRelationPO> drawerClearanceConfigRelationPOList
= drawerClearanceConfigRelationMapper.selectByDrawerBgIds(Collections.singletonList(drawerId), rootAccountId);
if (CollectionUtils.isEmpty(drawerClearanceConfigRelationPOList)) {
return null;
}
DrawerClearanceConfigRelationPO drawerClearanceConfigRelationPO = drawerClearanceConfigRelationPOList.get(0);
DrawerClearanceConfigPO drawerClearanceConfigPO = drawerClearanceConfigMapper.selectById(drawerClearanceConfigRelationPO.getConfigId(), rootAccountId);
return DrawerClearanceConfigConverter.toDrawerClearanceConfig(drawerClearanceConfigPO);
}
/**
* 通過配置ID刪除一個配置。
*
* @param configId 配置的唯一標識ID
*/
@Override
@Transactional(transactionManager = "dcsModelTransactionManager", rollbackFor = {Exception.class}, propagation = Propagation.REQUIRED)
public void removeConfigById(long configId, long rootAccountId) {
LOGGER.message("removeConfigById").with("configId", configId).with("rootAccountId", rootAccountId).info();
drawerClearanceConfigMapper.deleteById(configId, rootAccountId);
drawerClearanceConfigRelationMapper.deleteByConfigId(configId, rootAccountId);
}
}
5 一些 BP
-
聚合之間的關聯要使用 id
-
聚合是某個實體
-
事務的粒度就是一個聚合,多個聚合之間只能通過領域事件保證最終一致性
-
聚合設計要儘量小,如果一個實體不是根實體,但同時需要被外界直接訪問到,那麼這個實體不應該在這個聚合中,應該獨立成新的聚合。
-
聚合根作爲外界訪問的入口
-
值對象和實體的 equal 和 hash 方法需要重寫
-
常見的 insert、select、update、delete 都屬於 SQL 語法,使用這幾個詞相當於和 DB 底層實現做了綁定。相反,我們應該把 Repository 當成一箇中性的類似 Collection 的接口,使用語法如 find、save、remove。
-
讓 Domain Service 與 Repository 打交道,而不是讓領域模型 Entity 與 Repository 打交道
原文:https://juejin.cn/post/7404739357083697188
作者:AI 改變世界嗎
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/zjjXW7awneDppIaQ2DcC1w