《結(jié)合DDD講清楚編寫技術(shù)方案的七大維度》再討論
JAVA前線
歡迎大家關(guān)注公眾號「JAVA前線」查看更多精彩分享,主要內(nèi)容包括源碼分析、實際應(yīng)用、架構(gòu)思維、職場分享、產(chǎn)品思考等等,同時也非常歡迎大家加我微信「java_front」一起交流學(xué)習
1 前文回顧
我在之前文章《結(jié)合DDD講清楚編寫技術(shù)方案七大維度》介紹了從零到一使用DDD方法論搭建項目的七個步驟:
四色分領(lǐng)域 用例看功能 流程三劍客 領(lǐng)域與數(shù)據(jù) 縱橫做設(shè)計 分層看架構(gòu) 接口看對接
四色分領(lǐng)域介紹了使用四色分析法將一個整體需求拆分為不同領(lǐng)域,這是DDD方法論核心思想。四色分析法同樣可以用在子域或者限界上下文中,直到拆分出可以得心應(yīng)手處理之邊界為止。
用例看功能介紹了當領(lǐng)域劃分完成之后,使用用例圖描述系統(tǒng)功能。用例圖不關(guān)心實現(xiàn)細節(jié),而是從外部視角描述系統(tǒng)功能,即使不了解實現(xiàn)細節(jié)的人,通過用例圖也可以快速了解系統(tǒng)功能。
流程三劍客介紹了使用活動圖、順序圖、狀態(tài)機圖三種流程類型的圖示描述系統(tǒng),三種圖各有特點:活動圖著重描述邏輯分支,順序圖著重描述時間線索,狀態(tài)機圖著重描述狀態(tài)流轉(zhuǎn)。
領(lǐng)域與數(shù)據(jù)介紹了如何區(qū)分領(lǐng)域模型和數(shù)據(jù)模型。二者重要區(qū)別是值對象存儲方式。領(lǐng)域模型在包含值對象同時,也保留了值對象的業(yè)務(wù)含義,數(shù)據(jù)模型可以使用更加松散的結(jié)構(gòu)保存值對象,簡化數(shù)據(jù)庫設(shè)計。
縱橫做設(shè)計介紹了縱向做隔離,橫向做編排。復(fù)雜業(yè)務(wù)之所以復(fù)雜,一個重要原因是涉及角色或者類型較多,很難平鋪直敘地進行設(shè)計,所以我們需要增加分析維度。其中最常見的是增加橫向和縱向兩個維度。
分層看架構(gòu)介紹了系統(tǒng)架構(gòu)分為兩個層次,第一種層次指本項目在整個公司位于哪一層。持久層、緩存層、中間件、業(yè)務(wù)中臺、服務(wù)層、網(wǎng)關(guān)層、客戶端和代理層是常見分層架構(gòu)。第二種層次指項目代碼結(jié)構(gòu),一般可以分為接口層,訪問層,業(yè)務(wù)層,領(lǐng)域?qū)?,整合層和基礎(chǔ)層。
接口看對接介紹了一個接口代碼編寫完成后,這個接口如何調(diào)用,輸入和輸出參數(shù)是什么,這些問題需要在接口文檔中得到回答。
本文沿用上文中足球運動員管理系統(tǒng),主要從兩個維度對上文進行擴充,第一個維度是將DDD中一些概念與上文進行映射,例如領(lǐng)域、子域、限界上下文、實體、值對象、聚合與領(lǐng)域事件。第二個維度是展示DDD項目結(jié)構(gòu)層次。
2 領(lǐng)域、子域與限界上下文
2.1 核心概念
這三個詞雖然不同但是實際上都是在描述范圍這個概念。正如牛頓三定律有其適用范圍,程序中變量有其作用域一樣,DDD方法論也會將整體業(yè)務(wù)拆分成不同范圍,在同一個范圍內(nèi)進行才可以進行分析和處理。
上文實例中領(lǐng)域是足球,子域包括合同、醫(yī)療、訓(xùn)練、比賽、采訪,合同子域可以分為兩個限界上下文:轉(zhuǎn)會和簽約,醫(yī)療子域可以分為兩個限界上下文:體檢和傷病。

領(lǐng)域可以劃分子領(lǐng)域,子域可以再劃分子子域,限界上下文本質(zhì)上是一種子子域,那么在業(yè)務(wù)分解時一個業(yè)務(wù)模塊到底是領(lǐng)域、子域還是限界上下文?
這取決于看待這個模塊的角度。你認為整體可能是別人的局部,你認為的局部可能是別人的整體,叫什么名字不重要,最重要的是按照高內(nèi)聚原則將業(yè)務(wù)高度相關(guān)的模塊收斂。
2.2 限界上下文
限界上下文(Bounded contenxt)比較難理解,我們可以四個維度分析:
第一個維度是限界上下文本身含義。限界表示了規(guī)定一個邊界,上下文表示在這個邊界內(nèi)使用相同語義對象。例如goods這個詞,在商品邊界內(nèi)被稱為商品,但是快遞邊界內(nèi)被稱為貨物。
第二個維度是子域與限界上下文關(guān)系。子域可以對應(yīng)一個,也可以對應(yīng)多個限界上下文。如果子域劃分足夠小,那么就是限界上下文。如果子域可以再細分,那么可以劃分多個限界上下文。
第三維度是服務(wù)如何劃分。子域和限界上下文都可以作為微服務(wù),這里微服務(wù)是指獨立部署的程序進程,具體拆分到什么維度是根據(jù)業(yè)務(wù)需要、開發(fā)資源、維護成本、技術(shù)實力等因素綜合考量。如果按照子域進行微服務(wù)劃分可以拆分為:
基礎(chǔ)服務(wù):player-core-service 合同服務(wù):contract-core-service 醫(yī)療服務(wù):medical-core-service 訓(xùn)練服務(wù):training-core-service 比賽服務(wù):game-core-service 采訪服務(wù):interview-core-service
如果按照限界上下文進行微服務(wù)劃分,合同和醫(yī)療服務(wù)可以再拆分:
基礎(chǔ)合同服務(wù):contract-base-service 轉(zhuǎn)會合同服務(wù):contract-transfer-service 簽約合同服務(wù):contract-signing-service 基礎(chǔ)醫(yī)療服務(wù):medical-base-service 傷病醫(yī)療服務(wù):medical-injury-service 體檢醫(yī)療服務(wù):medical-exam-service
第四個維度是交互維度。在同一個限界上下文中實體對象和值對象可以自由交流,在不同限界上下文中必須通過聚合根進行交流。聚合根可以理解為一個按照業(yè)務(wù)聚合的代理對象。
例如產(chǎn)品經(jīng)理作為需求收口人,任何需求應(yīng)該先提給產(chǎn)品經(jīng)理,通過產(chǎn)品經(jīng)理整合后再提給程序員,而不是直接提給開發(fā)人員。
3 實體、值對象與聚合
領(lǐng)域模型分為三類:實體、值對象和聚合。實體是具有唯一標識的對象,唯一標識會伴隨實體對象整個生命周期并且不可變更。值對象本質(zhì)上是屬性的集合,沒有唯一標識。
聚合包括聚合根和聚合邊界兩個概念,聚合根可以理解為一個按照業(yè)務(wù)聚合的代理對象,一個限界上下文企圖訪問另一個限界上下文內(nèi)部對象,必須通過聚合根進行訪問。
3.1 數(shù)據(jù)維度
領(lǐng)域模型與數(shù)據(jù)模型一個重要的區(qū)別是值對象存儲方式。領(lǐng)域?qū)ο笤诎祵ο蟮耐瑫r也保留了值對象的業(yè)務(wù)含義,而數(shù)據(jù)對象可以使用更加松散的結(jié)構(gòu)保存值對象,簡化數(shù)據(jù)庫設(shè)計。
如果需要管理足球運動員基本信息和比賽數(shù)據(jù),對應(yīng)領(lǐng)域模型和數(shù)據(jù)模型應(yīng)該如何設(shè)計?姓名、身高、體重是一名運動員本質(zhì)屬性,加上唯一編號可以對應(yīng)實體對象。
跑動距離,傳球成功率,進球數(shù)是運動員比賽表現(xiàn),這些屬性的集合可以對應(yīng)值對象。

3.2 代碼維度
3.2.1 數(shù)據(jù)對象
PO(Persistent Object)直接與數(shù)據(jù)庫交互:
public class FootballPlayerPO {
// 運動員ID
private Long id;
// 運動員姓名
private String name;
// 運動員身高
private Integer height;
// 運動員體重
private Integer weight;
// 比賽表現(xiàn)(JSON)
private String gamePerformance;
// 創(chuàng)建人
private String creator;
// 修改人
private String updator;
// 創(chuàng)建時間
private Date createTime;
// 修改時間
private Date updateTime;
}
3.2.2 值對象
VO(Value Object)本質(zhì)上是屬性之集合,其不具有唯一標識:
public class GamePerformanceVO {
// 跑動距離
private Double runDistance;
// 傳球成功率
private Double passSuccess;
// 進球數(shù)
private Integer scoreNum;
}
public class MaintainVO {
// 創(chuàng)建人
private String creator;
// 修改人
private String updator;
// 創(chuàng)建時間
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;
// 比賽表現(xiàn)值對象
private GamePerformanceVO gamePerformanceVO;
}
3.2.4 聚合對象
Agg(Aggregate)可以理解為一個按照業(yè)務(wù)聚合的代理對象,任何訪問本限界上下文對象必須經(jīng)過聚合。實踐維度可以理解為充血模型版本BO,聚合對象中可以編寫業(yè)務(wù)邏輯:
public class FootballPlayerSimpleResultAgg {
// 運動員ID
private Long playerId;
// 運動員姓名
private String playerName;
}
public class FootballPlayerReadAgg implements BizValidator {
// 運動員ID
private Long playerId;
// 頁數(shù)
private Integer pageNum;
// 條數(shù)
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 數(shù)據(jù)傳輸對象
DTO(Data Transfer Object)用于接收或傳輸外部數(shù)據(jù),只應(yīng)該暴露必要信息:
public class FootballPlayerCreateDTO {
// 運動員姓名
private String name;
// 運動員身高
private Integer height;
// 運動員體重
private Integer weight;
// 跑動距離
private Double runDistance;
// 傳球成功率
private Double passSuccess;
// 進球數(shù)
private Integer scoreNum;
// 創(chuàng)建人
private String creator;
// 創(chuàng)建時間
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;
// 進球數(shù)
private Integer scoreNum;
// 修改人
private String updator;
// 修改時間
private Date updateTime;
}
public class FootballPlayerQueryDTO {
// 運動員ID
private Long playerId;
// 頁數(shù)
private Integer pageNum;
// 條數(shù)
private Integer size;
}
public class FootballPlayerSimpleResultDTO {
// 運動員ID
private Long playerId;
// 運動員姓名
private String playerName;
}
4 領(lǐng)域事件
當某個領(lǐng)域發(fā)生一件事情時,如果其它領(lǐng)域有后續(xù)動作跟進,我們把這件事情稱為領(lǐng)域事件,這個事件需要被感知。
球員比賽受傷,這是比賽域事件,但是醫(yī)療和訓(xùn)練域是需要感知的,那么比賽域發(fā)出一個事件,醫(yī)療和訓(xùn)練域會訂閱。球員比賽取得進球,這也是比賽域事件,但是訓(xùn)練和合同域也會關(guān)注這個事件,所以比賽域也會發(fā)出一個比賽進球事件,訓(xùn)練和合同域會訂閱。
通過事件交互有一個問題需要注意,通過事件訂閱實現(xiàn)業(yè)務(wù)只能采用最終一致性,需要放棄強一致性,可能會引入新的復(fù)雜度需要權(quán)衡。
同一個進程間事件交互可以用EventBus,跨進程事件交互可以用RocketMQ等消息中間件。
5 代碼結(jié)構(gòu)
5.1 六層結(jié)構(gòu)
DDD代碼實現(xiàn)方案不盡相同,我認為不能為使用DDD而是使用DDD,而是應(yīng)該根據(jù)實際情況選擇當前最合適的方案。但是無論是什么方案都需要遵循合理分層這個原則:

(1) API
接口層:提供面向外部接口聲明、DTO
(2) controller
訪問層:提供HTTP訪問入口
(3) service
業(yè)務(wù)層:領(lǐng)域?qū)雍蜆I(yè)務(wù)層都包含業(yè)務(wù),業(yè)務(wù)層可以組合不同領(lǐng)域業(yè)務(wù),并且可以實現(xiàn)流控、監(jiān)控、日志、權(quán)限功能,相較于領(lǐng)域?qū)痈S富
(4) domain
領(lǐng)域?qū)樱禾峁〦ntity、VO、Agg、事件,聚合對象使用充血模型
(5) integration
整合層:訪問外部限界上下文服務(wù),解析為本限界上下文聚合對象
(6) infrastructure
基礎(chǔ)層:提供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
> mapper5.3 如何取舍
上述項目有六層結(jié)構(gòu),那么必然帶來層次間調(diào)用對象互相轉(zhuǎn)換這個問題:
adapter1接收外部請求(DTO)需要轉(zhuǎn)換成(Agg)
adapter2處于業(yè)務(wù)層(操作數(shù)據(jù)庫)(Agg)需要轉(zhuǎn)換成(PO)
adapter3處于對外業(yè)務(wù)層(暴露RPC)(Agg)需要轉(zhuǎn)換成(DTO)
adapter4處于整合層(訪問外部RPC)(DTO)需要轉(zhuǎn)換成(Agg)對象轉(zhuǎn)換會帶來兩個問題:第一個是代碼復(fù)雜度增加,第二個是有一定性能損耗。這也是分層結(jié)構(gòu)必須要付出之代價。
因為每層對象看似相同(具有相同屬性或者結(jié)構(gòu))但是語義和角色完全不同,每一層可以為對象新增本層之特性,相較于使用一個對象貫穿始終,可擴展性顯著提升。
6 文章總結(jié)
第一章節(jié)回顧《結(jié)合DDD講清楚編寫技術(shù)方案七大維度》這篇文章并且提出擴展兩個維度:概念映射與代碼結(jié)構(gòu),第二三四章節(jié)對應(yīng)擴展第一個維度概念映射,第五章節(jié)對應(yīng)擴展第二個維度代碼結(jié)構(gòu),希望本文對大家有所幫助。
JAVA前線
歡迎大家關(guān)注公眾號「JAVA前線」查看更多精彩分享,主要內(nèi)容包括源碼分析、實際應(yīng)用、架構(gòu)思維、職場分享、產(chǎn)品思考等等,同時也非常歡迎大家加我微信「java_front」一起交流學(xué)習
