《結合DDD講清楚編寫技術方案的七大維度》再討論
歡迎大家關注公眾號「JAVA前線」檢視更多精彩分享文章,主要包括原始碼分析、實際應用、架構思維、職場分享、產品思考等等,同時歡迎大家加我個人微信「java_front」一起交流學習
1 六大原則
1 前文回顧
我在之前文章《結合DDD講清楚編寫技術方案七大維度》介紹了從零到一使用DDD方法論搭建專案的七個步驟:
- 四色分領域
- 用例看功能
- 流程三劍客
- 領域與資料
- 縱橫做設計
- 分層看架構
- 介面看對接
四色分領域介紹了使用四色分析法將一個整體需求拆分為不同領域,這是DDD方法論核心思想。四色分析法同樣可以用在子域或者限界上下文中,直到拆分出可以得心應手處理之邊界為止。
用例看功能介紹了當領域劃分完成之後,使用用例圖描述系統功能。用例圖不關心實現細節,而是從外部視角描述系統功能,即使不瞭解實現細節的人,通過用例圖也可以快速瞭解系統功能。
流程三劍客介紹了使用活動圖、順序圖、狀態機圖三種流程型別的圖示描述系統,三種圖各有特點:活動圖著重描述邏輯分支,順序圖著重描述時間線索,狀態機圖著重描述狀態流轉。
領域與資料介紹瞭如何區分領域模型和資料模型。二者重要區別是值物件儲存方式。領域模型在包含值物件同時,也保留了值物件的業務含義,資料模型可以使用更加鬆散的結構儲存值物件,簡化資料庫設計。
縱橫做設計介紹了縱向做隔離,橫向做編排。複雜業務之所以複雜,一個重要原因是涉及角色或者型別較多,很難平鋪直敘地進行設計,所以我們需要增加分析維度。其中最常見的是增加橫向和縱向兩個維度。
分層看架構介紹了系統架構分為兩個層次,第一種層次指本專案在整個公司位於哪一層。持久層、快取層、中介軟體、業務中臺、服務層、閘道器層、客戶端和代理層是常見分層架構。第二種層次指專案程式碼結構,一般可以分為介面層,訪問層,業務層,領域層,整合層和基礎層。
介面看對接介紹了一個介面程式碼編寫完成後,這個介面如何呼叫,輸入和輸出引數是什麼,這些問題需要在介面文件中得到回答。
本文沿用上文中足球運動員管理系統,主要從兩個維度對上文進行擴充,第一個維度是將DDD中一些概念與上文進行對映,例如領域、子域、限界上下文、實體、值物件、聚合與領域事件。第二個維度是展示DDD專案結構層次。
2 領域、子域與限界上下文
2.1 核心概念
這三個詞雖然不同但是實際上都是在描述範圍這個概念。正如牛頓三定律有其適用範圍,程式中變數有其作用域一樣,DDD方法論也會將整體業務拆分成不同範圍,在同一個範圍內進行才可以進行分析和處理。
上文例項中領域是足球,子域包括合同、醫療、訓練、比賽、採訪,合同子域可以分為兩個限界上下文:轉會和簽約,醫療子域可以分為兩個限界上下文:體檢和傷病。
領域可以劃分子領域,子域可以再劃分子子域,限界上下文字質上是一種子子域,那麼在業務分解時一個業務模組到底是領域、子域還是限界上下文?
這取決於看待這個模組的角度。你認為整體可能是別人的區域性,你認為的區域性可能是別人的整體,叫什麼名字不重要,最重要的是按照高內聚原則將業務高度相關的模組收斂。
2.2 限界上下文
限界上下文(Bounded contenxt)比較難理解,我們可以四個維度分析:
第一個維度是限界上下文字身含義。限界表示了規定一個邊界,上下文表示在這個邊界內使用相同語義物件。例如goods這個詞,在商品邊界內被稱為商品,但是快遞邊界內被稱為貨物。
第二個維度是子域與限界上下文關係。子域可以對應一個,也可以對應多個限界上下文。如果子域劃分足夠小,那麼就是限界上下文。如果子域可以再細分,那麼可以劃分多個限界上下文。
第三維度是服務如何劃分。子域和限界上下文都可以作為微服務,這裡微服務是指獨立部署的程式程序,具體拆分到什麼維度是根據業務需要、開發資源、維護成本、技術實力等因素綜合考量。如果按照子域進行微服務劃分可以拆分為:
- 基礎服務:player-core-service
- 合同服務:contract-core-service
- 醫療服務:medical-core-service
- 訓練服務:training-core-service
- 比賽服務:game-core-service
- 採訪服務:interview-core-service
如果按照限界上下文進行微服務劃分,合同和醫療服務可以再拆分:
- 基礎合同服務:contract-base-service
- 轉會合同服務:contract-transfer-service
- 簽約合同服務:contract-signing-service
- 基礎醫療服務:medical-base-service
- 傷病醫療服務:medical-injury-service
- 體檢醫療服務:medical-exam-service
第四個維度是互動維度。在同一個限界上下文中實體物件和值物件可以自由交流,在不同限界上下文中必須通過聚合根進行交流。聚合根可以理解為一個按照業務聚合的代理物件。
例如產品經理作為需求收口人,任何需求應該先提給產品經理,通過產品經理整合後再提給程式設計師,而不是直接提給開發人員。
3 實體、值物件與聚合
領域模型分為三類:實體、值物件和聚合。實體是具有唯一標識的物件,唯一標識會伴隨實體物件整個生命週期並且不可變更。值物件本質上是屬性的集合,沒有唯一標識。
聚合包括聚合根和聚合邊界兩個概念,聚合根可以理解為一個按照業務聚合的代理物件,一個限界上下文企圖訪問另一個限界上下文內部物件,必須通過聚合根進行訪問。
3.1 資料維度
領域模型與資料模型一個重要的區別是值物件儲存方式。領域物件在包含值物件的同時也保留了值物件的業務含義,而資料物件可以使用更加鬆散的結構儲存值物件,簡化資料庫設計。
如果需要管理足球運動員基本資訊和比賽資料,對應領域模型和資料模型應該如何設計?姓名、身高、體重是一名運動員本質屬性,加上唯一編號可以對應實體物件。
跑動距離,傳球成功率,進球數是運動員比賽表現,這些屬性的集合可以對應值物件。
3.2 程式碼維度
3.2.1 資料物件
PO(Persistent Object)直接與資料庫互動:
public class FootballPlayerPO {
// 運動員ID
private Long id;
// 運動員姓名
private String name;
// 運動員身高
private Integer height;
// 運動員體重
private Integer weight;
// 比賽表現(JSON)
private String gamePerformance;
// 建立人
private String creator;
// 修改人
private String updator;
// 建立時間
private Date createTime;
// 修改時間
private Date updateTime;
}
3.2.2 值物件
VO(Value Object)本質上是屬性之集合,其不具有唯一標識:
public class GamePerformanceVO {
// 跑動距離
private Double runDistance;
// 傳球成功率
private Double passSuccess;
// 進球數
private Integer scoreNum;
}
public class MaintainVO {
// 建立人
private String creator;
// 修改人
private String updator;
// 建立時間
private Date createTime;
// 修改時間
private Date updateTime;
}
3.2.3 實體物件
Entity具有唯一標識,這個唯一標識會伴隨實體物件整個生命週期:
public class FootballPlayerEntity {
// 運動員ID
private Long id;
// 運動員姓名
private String name;
// 運動員身高
private Integer height;
// 運動員體重
private Integer weight;
// 比賽表現值物件
private GamePerformanceVO gamePerformanceVO;
}
3.2.4 聚合物件
Agg(Aggregate)可以理解為一個按照業務聚合的代理物件,任何訪問本限界上下文物件必須經過聚合。實踐維度可以理解為充血模型版本BO,聚合物件中可以編寫業務邏輯:
public class FootballPlayerSimpleResultAgg {
// 運動員ID
private Long playerId;
// 運動員姓名
private String playerName;
}
public class FootballPlayerReadAgg implements BizValidator {
// 運動員ID
private Long playerId;
// 頁數
private Integer pageNum;
// 條數
private Integer size;
@Override
public void validate() {
AssertUtil.notNull(playerId, new BizError);
AssertUtil.notBigger(size, 100, new BizError);
}
}
public class FootballPlayerWriteAgg implements BizValidator {
// 操作型別
private Integer maintainType;
// 維護資訊
private MaintainVO maintainInfo;
// 運動員資訊
private FootballPlayerEntity playInfo;
@Override
public void validate() {
AssertUtil.notNull(maintainType, new BizError);
AssertUtil.notNull(maintainInfo, new BizError);
AssertUtil.notNull(playInfo, new BizError);
if(maintainType == MaintainEnum.CREATE.getType()) {
AssertUtil.notNull(maintainInfo.getCreator(), new BizError);
AssertUtil.notNull(maintainInfo.getCreateTime(), new BizError);
}
if(maintainType == MaintainEnum.UPADTE.getType()) {
AssertUtil.notNull(maintainInfo.getUpdator(), new BizError);
AssertUtil.notNull(maintainInfo.getUpdateTime(), new BizError);
}
}
}
3.2.5 資料傳輸物件
DTO(Data Transfer Object)用於接收或傳輸外部資料,只應該暴露必要資訊:
public class FootballPlayerCreateDTO {
// 運動員姓名
private String name;
// 運動員身高
private Integer height;
// 運動員體重
private Integer weight;
// 跑動距離
private Double runDistance;
// 傳球成功率
private Double passSuccess;
// 進球數
private Integer scoreNum;
// 建立人
private String creator;
// 建立時間
private Date createTime;
}
public class FootballPlayerUpdateDTO {
// 運動員ID
private Long id;
// 運動員姓名
private String name;
// 運動員身高
private Integer height;
// 運動員體重
private Integer weight;
// 跑動距離
private Double runDistance;
// 傳球成功率
private Double passSuccess;
// 進球數
private Integer scoreNum;
// 修改人
private String updator;
// 修改時間
private Date updateTime;
}
public class FootballPlayerQueryDTO {
// 運動員ID
private Long playerId;
// 頁數
private Integer pageNum;
// 條數
private Integer size;
}
public class FootballPlayerSimpleResultDTO {
// 運動員ID
private Long playerId;
// 運動員姓名
private String playerName;
}
4 領域事件
當某個領域發生一件事情時,如果其它領域有後續動作跟進,我們把這件事情稱為領域事件,這個事件需要被感知。
球員比賽受傷,這是比賽域事件,但是醫療和訓練域是需要感知的,那麼比賽域發出一個事件,醫療和訓練域會訂閱。球員比賽取得進球,這也是比賽域事件,但是訓練和合同域也會關注這個事件,所以比賽域也會發出一個比賽進球事件,訓練和合同域會訂閱。
通過事件互動有一個問題需要注意,通過事件訂閱實現業務只能採用最終一致性,需要放棄強一致性,可能會引入新的複雜度需要權衡。
同一個程序間事件互動可以用EventBus,跨程序事件互動可以用RocketMQ等訊息中介軟體。
5 程式碼結構
5.1 六層結構
DDD程式碼實現方案不盡相同,我認為不能為使用DDD而是使用DDD,而是應該根據實際情況選擇當前最合適的方案。但是無論是什麼方案都需要遵循合理分層這個原則:
(1) API
介面層:提供面向外部介面宣告、DTO
(2) controller
訪問層:提供HTTP訪問入口
(3) service
業務層:領域層和業務層都包含業務,業務層可以組合不同領域業務,並且可以實現流控、監控、日誌、許可權功能,相較於領域層更豐富
(4) domain
領域層:提供Entity、VO、Agg、事件,聚合物件使用充血模型
(5) integration
整合層:訪問外部限界上下文服務,解析為本限界上下文聚合物件
(6) infrastructure
基礎層:提供PO、持久化能力
5.2 程式碼例項
如果player-core-service作為maven parent,那麼其具有以下maven module和分包:
> player-core-service
> player-core-api
> dto
> facade
> player-core-controller
> controller
> adapter1 (DTO > Agg)
> player-core-service
> bizService
> adapter2 (Agg > PO)
> facadeService
> adapter3 (Agg > DTO)
> player-core-domain
> vo
> entity
> agg
> event
> player-core-integration
> proxy
> adapter4 (DTO > Agg)
> player-core-infrastructure
> po
> mapper
5.3 如何取捨
上述專案有六層結構,那麼必然帶來層次間呼叫物件互相轉換這個問題:
adapter1接收外部請求(DTO)需要轉換成(Agg)
adapter2處於業務層(操作資料庫)(Agg)需要轉換成(PO)
adapter3處於對外業務層(暴露RPC)(Agg)需要轉換成(DTO)
adapter4處於整合層(訪問外部RPC)(DTO)需要轉換成(Agg)
物件轉換會帶來兩個問題:第一個是程式碼複雜度增加,第二個是有一定效能損耗。這也是分層結構必須要付出之代價。
因為每層物件看似相同(具有相同屬性或者結構)但是語義和角色完全不同,每一層可以為物件新增本層之特性,相較於使用一個物件貫穿始終,可擴充套件性顯著提升。
6 文章總結
第一章節回顧《結合DDD講清楚編寫技術方案七大維度》這篇文章並且提出擴充套件兩個維度:概念對映與程式碼結構,第二三四章節對應擴充套件第一個維度概念對映,第五章節對應擴充套件第二個維度程式碼結構,希望本文對大家有所幫助。
歡迎大家關注公眾號「JAVA前線」檢視更多精彩分享文章,主要包括原始碼分析、實際應用、架構思維、職場分享、產品思考等等,同時歡迎大家加我個人微信「java_front」一起交流學習
- 七種方法增強程式碼可擴充套件性(多圖詳解)
- 效能問題從發現到優化一般思路
- 《結合DDD講清楚編寫技術方案的七大維度》再討論
- 自定義validation註解:解決多條件複雜動態校驗問題
- OAuth2.0原理圖解:第三方網站為什麼可以使用微信登入
- 長文圖解七種負載均衡策略
- 長文多圖:結合DDD講清楚編寫技術方案的七大維度
- 長文多圖:結合DDD講清楚編寫技術方案的七大維度
- 為什麼網際網路公司不招大齡碼農?
- 多圖詳解:如何不停服分庫分表
- 面試官問一個數據表字段怎麼表示多種業務含義?我愣了五分鐘
- 面試官問如何結合Apollo構建動態執行緒池,我們聊了二十分鐘
- 執行緒池原始碼解析系列:為什麼要使用位運算表示執行緒池狀態
- 面試官問一個數據表字段怎麼表示多種業務含義?我愣了五分鐘
- 面試官問單表資料量大一定要分庫分表嗎?我們用六個字和十張圖回答
- 面試官問金字塔思維如何應用在技術系統,我們聊了三十分鐘
- 一個公式看懂:為什麼DUBBO執行緒池會打滿
- 什麼是服務降級?Dubbo服務降級不能降級哪類異常?
- 長文詳解:DUBBO原始碼使用了哪些設計模式
- 長文圖解:單張表資料量太大問題怎麼解決?請記住這六個字