DDD實(shí)戰(zhàn) - Repository模式的妙用
大家好,我是飄渺。今天我們繼續(xù)更新DDD(領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)) & 微服務(wù)系列。
在之前的文章中,我們探討了如何在DDD中結(jié)構(gòu)化應(yīng)用程序。我們了解到,在DDD中通常將應(yīng)用程序分為四個(gè)層次,分別為用戶接口層(Interface Layer),應(yīng)用層(Application Layer),領(lǐng)域?qū)樱―omain Layer),和基礎(chǔ)設(shè)施層(Infrastructure Layer)。此外,在用戶注冊(cè)的主題中,我們簡要地提及了資源庫模式。然而,那時(shí)我們并沒有深入探討。今天,我將為大家詳細(xì)介紹資源庫模式,這在DDD中是一個(gè)非常重要的概念。
1. 傳統(tǒng)開發(fā)流程分析
首先,讓我們回顧一下傳統(tǒng)的以數(shù)據(jù)庫為中心的開發(fā)流程。
在這種開發(fā)流程中,開發(fā)者通常會(huì)創(chuàng)建Data Access Object(DAO)來封裝對(duì)數(shù)據(jù)庫的操作。DAO的主要優(yōu)勢(shì)在于它能夠簡化構(gòu)建SQL查詢、管理數(shù)據(jù)庫連接和事務(wù)等底層任務(wù)。這使得開發(fā)者能夠?qū)⒏嗟木Ψ旁跇I(yè)務(wù)邏輯的編寫上。然而,DAO雖然簡化了操作,但仍然直接處理數(shù)據(jù)庫和數(shù)據(jù)模型。
值得注意的是,Uncle Bob在《代碼整潔之道》一書中,通過一些術(shù)語生動(dòng)地描述了這個(gè)問題。他將系統(tǒng)元素分為三類:
硬件(Hardware): 指那些一旦創(chuàng)建就不可(或難以)更改的元素。在開發(fā)背景下,數(shù)據(jù)庫被視為“硬件”,因?yàn)橐坏┻x擇了一種數(shù)據(jù)庫,例如MySQL,轉(zhuǎn)向另一種數(shù)據(jù)庫,如MongoDB,通常會(huì)帶來巨大的成本和挑戰(zhàn)。
軟件(Software): 指那些創(chuàng)建后可以隨時(shí)修改的元素。開發(fā)者應(yīng)該致力于使業(yè)務(wù)代碼作為“軟件”,因?yàn)闃I(yè)務(wù)需求和規(guī)則總是在不斷變化,因此代碼也應(yīng)該具有相應(yīng)的靈活性和可調(diào)整性。
固件(Firmware): 是那些與硬件緊密耦合,但具有一定的軟性特點(diǎn)的軟件。例如,路由器的固件或Android固件。它們?yōu)橛布峁┏橄螅ǔV贿m用于特定類型的硬件。
通過理解這些術(shù)語,我們可以認(rèn)識(shí)到數(shù)據(jù)庫應(yīng)視為“硬件”,而DAO在本質(zhì)上屬于“固件”。然而,我們的目標(biāo)是使我們的代碼保持像“軟件”那樣的靈活性。但是,當(dāng)業(yè)務(wù)代碼過于依賴于“固件”時(shí),它會(huì)受到限制,變得難以更改。
讓我們通過一個(gè)具體的例子來進(jìn)一步理解這個(gè)概念。下面是一個(gè)簡單的代碼片段,展示了一個(gè)對(duì)象如何依賴于DAO(也就是依賴于數(shù)據(jù)庫):
private OrderDAO orderDAO;
public Long addOrder(RequestDTO request) {
// 此處省略很多拼裝邏輯
OrderDO orderDO = new OrderDO();
orderDAO.insertOrder(orderDO);
return orderDO.getId();
}
public void updateOrder(OrderDO orderDO, RequestDTO updateRequest) {
orderDO.setXXX(XXX); // 省略很多
orderDAO.updateOrder(orderDO);
}
public void doSomeBusiness(Long id) {
OrderDO orderDO = orderDAO.getOrderById(id);
// 此處省略很多業(yè)務(wù)邏輯
}
上面的代碼片段看似無可厚非,但假設(shè)在未來我們需要加入緩存邏輯,代碼則需要改為如下:
private OrderDAO orderDAO;
private Cache cache;
public Long addOrder(RequestDTO request) {
// 此處省略很多拼裝邏輯
OrderDO orderDO = new OrderDO();
orderDAO.insertOrder(orderDO);
cache.put(orderDO.getId(), orderDO);
return orderDO.getId();
}
public void updateOrder(OrderDO orderDO, RequestDTO updateRequest) {
orderDO.setXXX(XXX); // 省略很多
orderDAO.updateOrder(orderDO);
cache.put(orderDO.getId(), orderDO);
}
public void doSomeBusiness(Long id) {
OrderDO orderDO = cache.get(id);
if (orderDO == null) {
orderDO = orderDAO.getOrderById(id);
}
// 此處省略很多業(yè)務(wù)邏輯
}
可以看到,插入緩存邏輯后,原本簡單的代碼變得復(fù)雜。原本一行代碼現(xiàn)在至少需要三行。隨著代碼量的增加,如果你在某處忘記查看緩存或忘記更新緩存,可能會(huì)導(dǎo)致輕微的性能下降或者更糟糕的是,緩存和數(shù)據(jù)庫的數(shù)據(jù)不一致,從而導(dǎo)致bug。這種問題隨著代碼量和復(fù)雜度的增長會(huì)變得更加嚴(yán)重,這就是軟件被“固化”的后果。
因此,我們需要一個(gè)設(shè)計(jì)模式來隔離我們的軟件(業(yè)務(wù)邏輯)與固件/硬件(DAO、數(shù)據(jù)庫),以提高代碼的健壯性和可維護(hù)性。這個(gè)模式就是DDD中的資源庫模式(Repository Pattern)。
2. 深入理解資源庫模式
在DDD(領(lǐng)域驅(qū)動(dòng)設(shè)計(jì))中,資源庫起著至關(guān)重要的作用。資源庫的核心任務(wù)是為應(yīng)用程序提供統(tǒng)一的數(shù)據(jù)訪問入口。它允許我們以一種與底層數(shù)據(jù)存儲(chǔ)無關(guān)的方式,來存儲(chǔ)和檢索領(lǐng)域?qū)ο?。這對(duì)于將業(yè)務(wù)邏輯與數(shù)據(jù)訪問代碼解耦是非常有價(jià)值的。
2.1 資源庫模式在架構(gòu)中的位置
資源庫是一種廣泛應(yīng)用的架構(gòu)模式。事實(shí)上,當(dāng)你使用諸如Hibernate、Mybatis這樣的ORM框架時(shí),你已經(jīng)在間接地使用資源庫模式了。資源庫扮演著對(duì)象的提供者的角色,并且處理對(duì)象的持久化。讓我們看一下持久化:持久化意味著將數(shù)據(jù)保存在一個(gè)持久媒介,比如關(guān)系型數(shù)據(jù)庫或NoSQL數(shù)據(jù)庫,這樣即使應(yīng)用程序終止,數(shù)據(jù)也不會(huì)丟失。這些持久化媒介具有不同的特性和優(yōu)點(diǎn),因此,資源庫的實(shí)現(xiàn)會(huì)依據(jù)所使用的媒介有所不同。
資源庫的設(shè)計(jì)通常包括兩個(gè)主要組成部分:定義和實(shí)現(xiàn)。定義部分是一個(gè)抽象接口,它只描述了我們可以對(duì)數(shù)據(jù)執(zhí)行哪些操作,而不涉及具體如何執(zhí)行它們。實(shí)現(xiàn)部分則是這些操作的具體實(shí)現(xiàn)。它依賴于一個(gè)特定的持久化媒介,并可能需要與特定的技術(shù)進(jìn)行交互。
2.2 領(lǐng)域?qū)优c基礎(chǔ)設(shè)施層
根據(jù)DDD的分層架構(gòu),領(lǐng)域?qū)影信c業(yè)務(wù)領(lǐng)域有關(guān)的元素,包括實(shí)體、值對(duì)象和聚合。領(lǐng)域?qū)颖硎緲I(yè)務(wù)的核心概念和邏輯。
另一方面,基礎(chǔ)設(shè)施層包含支持其他層的通用技術(shù),比如數(shù)據(jù)庫訪問、文件系統(tǒng)交互等。
資源庫模式很好地適用于這種分層結(jié)構(gòu)。資源庫的定義部分,即抽象接口,位于領(lǐng)域?qū)樱驗(yàn)樗苯优c領(lǐng)域?qū)ο蠼换?。而資源庫的實(shí)現(xiàn)部分則屬于基礎(chǔ)設(shè)施層,它處理具體的數(shù)據(jù)訪問邏輯。
以DailyMart系統(tǒng)中的CustomerUser為例
圖片
如上圖所示,CustomerUserRepository是資源庫接口,位于領(lǐng)域?qū)?,操作的?duì)象是CustomerUser聚合根。CustomerUserRepositoryImpl是資源庫的實(shí)現(xiàn)部分,位于基礎(chǔ)設(shè)施層。這個(gè)實(shí)現(xiàn)部分操作的是持久化對(duì)象,這就需要在基礎(chǔ)設(shè)施層中有一個(gè)組件來處理領(lǐng)域?qū)ο笈c數(shù)據(jù)對(duì)象的轉(zhuǎn)換,在之前的文章中已經(jīng)推薦使用工具mapstruct來實(shí)現(xiàn)這種轉(zhuǎn)換。
2.3 小結(jié)
資源庫是DDD中一個(gè)強(qiáng)大的概念,允許我們以一種整潔和一致的方式來處理數(shù)據(jù)訪問。通過將資源庫的定義放在領(lǐng)域?qū)?,并將其?shí)現(xiàn)放在基礎(chǔ)設(shè)施層,我們能夠有效地將業(yè)務(wù)邏輯與數(shù)據(jù)訪問代碼解耦,從而使應(yīng)用程序更加靈活和可維護(hù)。
3. 倉儲(chǔ)接口的設(shè)計(jì)原則
當(dāng)我們?cè)O(shè)計(jì)倉儲(chǔ)接口時(shí),目標(biāo)是創(chuàng)造一個(gè)清晰、可維護(hù)且松耦合的結(jié)構(gòu),這樣能夠讓應(yīng)用程序更加靈活和健壯。以下是倉儲(chǔ)接口設(shè)計(jì)的一些原則和最佳實(shí)踐:
- 避免使用底層實(shí)現(xiàn)語法命名接口方法:倉儲(chǔ)接口應(yīng)該與底層數(shù)據(jù)存儲(chǔ)實(shí)現(xiàn)保持解耦。使用像insert, select, update, delete這樣的詞語,這些都是SQL語法,等于是將接口與數(shù)據(jù)庫實(shí)現(xiàn)綁定。相反,應(yīng)該視倉儲(chǔ)為一個(gè)類似集合的抽象,使用更通用的詞匯,如 **find、save、remove**。特別注意,區(qū)分insert/add 和 update 本身就是與底層實(shí)現(xiàn)綁定的邏輯,有時(shí)候存儲(chǔ)方式(如緩存)并不區(qū)分這兩者。在這種情況下,使用一個(gè)中立的save接口,然后在具體的實(shí)現(xiàn)中根據(jù)需要調(diào)用insert或update。
- 使用領(lǐng)域?qū)ο笞鳛閰?shù)和返回值:倉儲(chǔ)接口位于領(lǐng)域?qū)?,因此它不?yīng)該暴露底層數(shù)據(jù)存儲(chǔ)的細(xì)節(jié)。當(dāng)?shù)讓哟鎯?chǔ)技術(shù)發(fā)生變化時(shí),領(lǐng)域模型應(yīng)保持不變。因此,倉儲(chǔ)接口應(yīng)以領(lǐng)域?qū)ο?,特別是聚合根(Aggregate Root)對(duì)象,作為參數(shù)和返回值。
- 避免過度通用化的倉儲(chǔ)模式:雖然一些ORM框架(如Spring Data和Entity Framework)提供了高度通用的倉儲(chǔ)接口,通過注解自動(dòng)實(shí)現(xiàn)接口,但這種做法在簡單場(chǎng)景下雖然方便,但通常缺乏擴(kuò)展性(例如,添加自定義緩存邏輯)。使用這種通用接口可能導(dǎo)致在未來的開發(fā)中遇到限制,甚至需要進(jìn)行大的重構(gòu)。但請(qǐng)注意,避免過度通用化并不意味著不能有基本的接口或通用的輔助類。
- 定義清晰的事務(wù)邊界:通常,事務(wù)應(yīng)該在應(yīng)用服務(wù)層開始和結(jié)束,而不是在倉儲(chǔ)層。這樣可以確保事務(wù)的范圍明確,并允許更好地控制事務(wù)的生命周期。
通過遵循上述原則和最佳實(shí)踐,我們可以創(chuàng)建一個(gè)倉儲(chǔ)接口,不僅與底層數(shù)據(jù)存儲(chǔ)解耦,還能支持領(lǐng)域模型的演變和應(yīng)用程序的可維護(hù)性。
4. Repository的代碼實(shí)現(xiàn)
在DailyMart項(xiàng)目中,為了實(shí)現(xiàn)DDD開發(fā)的最佳實(shí)踐,我們創(chuàng)建一個(gè)名為dailymart-ddd-spring-boot-starter的組件模塊,專門存放DDD相關(guān)的核心組件。這種做法簡潔地讓其他模塊通過引入此公共模塊來遵循DDD原則。
圖片
4.1 制定Marker接口類
Marker接口主要為類型定義和派生類分類提供標(biāo)識(shí),通常不包含任何方法。我們首先定義幾個(gè)核心的Marker接口。
public interface Identifiable<ID extends Identifier<?>> extends Serializable {
ID getId();
}
public interface Identifier<T> extends Serializable {
T getValue();
}
public interface Entity<ID extends Identifier<?>> extends Identifiable<ID> { }
public interface Aggregate<ID extends Identifier<?>> extends Entity<ID> { }
這里,聚合會(huì)實(shí)現(xiàn)Aggregate接口,而實(shí)體會(huì)實(shí)現(xiàn)Entity接口。聚合本質(zhì)上是一種特殊的實(shí)體,這種結(jié)構(gòu)使邏輯更加清晰。另外,我們引入了Identifier接口來表示實(shí)體的唯一標(biāo)識(shí)符,它將唯一標(biāo)識(shí)符視為值對(duì)象,這是DDD中常見的做法。如下面所示的案例
public class OrderId implements Identifier<Long> {
@Serial
private static final long serialVersionUID = -8658575067669691021L;
public Long id;
public OrderId(Long id){
this.id = id;
}
@Override
public Long getValue() {
return id;
}
}
4.2 創(chuàng)建通用Repository接口
接下來,我們定義一個(gè)基礎(chǔ)的Repository接口。
public interface Repository <T extends Aggregate<ID>, ID extends Identifier<?>> {
T find(ID id);
void remove(T aggregate);
void save(T aggregate);
}
業(yè)務(wù)特定的接口可以在此基礎(chǔ)上進(jìn)行擴(kuò)展。例如,對(duì)于訂單,我們可以添加計(jì)數(shù)和分頁查詢。
public interface OrderRepository extends Repository<Order, OrderId> {
// 自定義Count接口,在這里OrderQuery是一個(gè)自定義的DTO
Long count(OrderQuery query);
// 自定義分頁查詢接口
Page<Order> query(OrderQuery query);
}
請(qǐng)注意,Repository的接口定義位于Domain層,而具體的實(shí)現(xiàn)則位于Infrastructure層。
4.3 實(shí)施Repository的基本功能
下面是一個(gè)簡單的Repository實(shí)現(xiàn)示例。注意,OrderRepositoryNativeImpl在Infrastructure層。
@Repository
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Slf4j
public class OrderRepositoryNativeImpl implements OrderRepository {
private final OrderMapper orderMapper;
private final OrderItemMapper orderItemMapper;
private final OrderConverter orderConverter;
private final OrderItemConverter orderItemConverter;
@Override
public Order find(OrderId orderId) {
OrderDO orderDO = orderMapper.selectById(orderId.getValue());
return orderConverter.fromData(orderDO);
}
@Override
public void save(Order aggregate) {
if(aggregate.getId() != null && aggregate.getId().getValue() > 0){
// update
OrderDO orderDO = orderConverter.toData(aggregate);
orderMapper.updateById(orderDO);
}else{
// insert
OrderDO orderDO = orderConverter.toData(aggregate);
orderMapper.insert(orderDO);
aggregate.setId(orderConverter.fromData(orderDO).getId());
}
}
...
}
這段代碼展示了一個(gè)常見的模式:Entity/Aggregate轉(zhuǎn)換為Data Object(DO),然后使用Data Access Object(DAO)根據(jù)業(yè)務(wù)邏輯執(zhí)行相應(yīng)操作。在操作完成后,如果需要,還可以將DO轉(zhuǎn)換回Entity。代碼很簡單,唯一需要注意的是save方法,需要根據(jù)Aggregate的ID是否存在且大于0來判斷一個(gè)Aggregate是否需要更新還是插入。
4.4 Repository復(fù)雜實(shí)現(xiàn)
處理單一實(shí)體的Repository實(shí)現(xiàn)通常較為直接,但當(dāng)聚合中包含多個(gè)實(shí)體時(shí),操作的復(fù)雜性會(huì)增加。主要的問題在于,在單次操作中,并不是聚合中的所有實(shí)體都需要變更,而使用簡單的實(shí)現(xiàn)會(huì)導(dǎo)致許多不必要的數(shù)據(jù)庫操作。
以一個(gè)典型的場(chǎng)景為例:一個(gè)訂單中包含多個(gè)商品明細(xì)。如果修改了某個(gè)商品明細(xì)的數(shù)量,這會(huì)同時(shí)影響主訂單的總價(jià),但對(duì)其他商品明細(xì)則沒有影響。
圖片
若采用基礎(chǔ)的實(shí)現(xiàn)方法,會(huì)多出兩個(gè)不必要的更新操作,如下所示:
@Repository
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Slf4j
public class OrderRepositoryNativeImpl implements OrderRepository {
//省略其他邏輯
@Override
public void save(Order aggregate) {
if(aggregate.getId() != null && aggregate.getId().getValue() > 0){
// 每次都將Order和所有LineItem全量更新
OrderDO orderDO = orderConverter.toData(aggregate);
orderMapper.updateById(orderDO);
for(OrderItem orderItem : aggregate.getOrderItems()){
save(orderItem);
}
}else{
//省略插入邏輯
}
}
private void save(OrderItem orderItem) {
if (orderItem.getId() != null && orderItem.getId().getValue() > 0) {
OrderItemDO orderItemDO = orderItemConverter.toData(orderItem);
orderItemMapper.updateById(orderItemDO);
} else {
OrderItemDO orderItemDO = orderItemConverter.toData(orderItem);
orderItemMapper.insert(orderItemDO);
orderItem.setItemId(orderItemConverter.fromData(orderItemDO).getId());
}
}
}
在此示例中,會(huì)執(zhí)行4個(gè)UPDATE操作,而實(shí)際上只需2個(gè)。通常情況下,這個(gè)額外的開銷并不嚴(yán)重,但如果非Aggregate Root的實(shí)體數(shù)量很大,這會(huì)導(dǎo)致大量不必要的寫操作。
4.5 變更追蹤(Change-Tracking)
針對(duì)上述問題,核心在于Repository接口的限制使得調(diào)用者只能操作Aggregate Root,而不能單獨(dú)操作非Aggregate Root的實(shí)體。這與直接調(diào)用DAO的方式有顯著差異。
一種解決方案是通過變更追蹤能力來識(shí)別哪些實(shí)體有變更,并且僅對(duì)這些變更過的實(shí)體執(zhí)行操作。這樣,先前需要手動(dòng)判斷的代碼邏輯現(xiàn)在可以通過變更追蹤來自動(dòng)實(shí)現(xiàn),讓開發(fā)者真正只關(guān)注聚合的操作。以前面的示例為例,通過變更追蹤,系統(tǒng)可以判斷出只有OrderItem2和Order發(fā)生了變化,因此只需要生成兩個(gè)UPDATE操作。
變更追蹤有兩種主流實(shí)現(xiàn)方式:
- 基于快照Snapshot的方案: 數(shù)據(jù)從數(shù)據(jù)庫提取后,在內(nèi)存中保存一份快照,然后在將數(shù)據(jù)寫回時(shí)與快照進(jìn)行比較。Hibernate是采用此種方法的常見實(shí)現(xiàn)。
- 基于代理Proxy的方案: 當(dāng)數(shù)據(jù)從數(shù)據(jù)庫提取后,通過織入的方式為所有setter方法增加一個(gè)切面來檢測(cè)setter是否被調(diào)用以及值是否發(fā)生變化。如果值發(fā)生變化,則將其標(biāo)記為“臟”(Dirty)。在保存時(shí),根據(jù)這個(gè)標(biāo)記來判斷是否需要更新。Entity Framework是一個(gè)采用此種方法的常見實(shí)現(xiàn)。
代理Proxy方案的優(yōu)勢(shì)是性能較高,幾乎沒有額外成本,但缺點(diǎn)是實(shí)現(xiàn)起來比較復(fù)雜,而且當(dāng)存在嵌套關(guān)系時(shí),不容易檢測(cè)到嵌套對(duì)象的變化(例如,子列表的增加和刪除),可能會(huì)導(dǎo)致bug。
而快照Snapshot方案的優(yōu)勢(shì)是實(shí)現(xiàn)相對(duì)簡單,成本在于每次保存時(shí)執(zhí)行全量比較(通常使用反射)以及保存快照的內(nèi)存消耗。
由于代理Proxy方案的復(fù)雜性,業(yè)界主流(包括EF Core)更傾向于使用基于Snapshot快照的方案。
此外,通過檢測(cè)差異,我們能識(shí)別哪些字段發(fā)生了改變,并僅更新這些發(fā)生變化的字段,從而進(jìn)一步降低UPDATE操作的開銷。無論是否在DDD上下文中,這個(gè)功能本身都是非常有用的。在DailyMart示例中,我們使用一個(gè)名為DiffUtils的工具類來輔助比較對(duì)象間的差異。
public class DiffUtilsTest {
@Test
public void diffObject() throws IllegalAccessException, IOException, ClassNotFoundException {
//實(shí)時(shí)對(duì)象
Order realObj = Order.builder()
.id(new OrderId(31L))
.customerId(100L)
.totalAmount(new BigDecimal(100))
.recipientInfo(new RecipientInfo("zhangsan","安徽省合肥市","123456"))
.build();
// 快照對(duì)象
Order snapshotObj = SnapshotUtils.snapshot(realObj);
snapshotObj.setId(new OrderId(2L));
snapshotObj.setTotalAmount(new BigDecimal(200));
EntityDiff diff = DiffUtils.diff(realObj, snapshotObj);
assertTrue(diff.isSelfModified());
assertEquals(2, diff.getDiffs().size());
}
}
詳細(xì)用法可以參考單元測(cè)試com.jianzh5.dailymart.module.order.infrastructure.util.DiffUtilsTest
通過變更追蹤的引入,我們能夠使聚合的Repository實(shí)現(xiàn)更加高效和智能。這允許開發(fā)人員將注意力集中在業(yè)務(wù)邏輯上,而不必?fù)?dān)心不必要的數(shù)據(jù)庫操作。
圖片
圖片
5 在DailyMart中集成變更追蹤
DailyMart系統(tǒng)內(nèi)涵蓋了一個(gè)訂單子域,該子域以O(shè)rder作為聚合根,并將OrderItem納入為其子實(shí)體。兩者之間構(gòu)成一對(duì)多的聯(lián)系。在對(duì)訂單進(jìn)行更新操作時(shí),變更追蹤顯得尤為關(guān)鍵。
下面展示的是DailyMart系統(tǒng)中關(guān)于變更追蹤的核心代碼片段。值得注意的是,這些代碼僅用于展示如何在倉庫模式中融入變更追蹤,并非訂單子域的完整實(shí)現(xiàn)。
AggregateRepositorySupport 類
該類是聚合倉庫的支持類,它管理聚合的變更追蹤。
@Slf4j
public abstract class AggregateRepositorySupport<T extends Aggregate<ID>, ID extends Identifier<?>> implements Repository<T, ID> {
@Getter
private final Class<T> targetClass;
// 讓 AggregateManager 去維護(hù) Snapshot
@Getter(AccessLevel.PROTECTED)
private AggregateManager<T, ID> aggregateManager;
protected AggregateRepositorySupport(Class<T> targetClass) {
this.targetClass = targetClass;
this.aggregateManager = AggregateManagerFactory.newInstance(targetClass);
}
/** Attach的操作就是讓Aggregate可以被追蹤 */
@Override
public void attach(@NotNull T aggregate) {
this.aggregateManager.attach(aggregate);
}
/** Detach的操作就是讓Aggregate停止追蹤 */
@Override
public void detach(@NotNull T aggregate) {
this.aggregateManager.detach(aggregate);
}
@Override
public T find(@NotNull ID id) {
T aggregate = this.onSelect(id);
if (aggregate != null) {
// 這里的就是讓查詢出來的對(duì)象能夠被追蹤。
// 如果自己實(shí)現(xiàn)了一個(gè)定制查詢接口,要記得單獨(dú)調(diào)用attach。
this.attach(aggregate);
}
return aggregate;
}
@Override
public void remove(@NotNull T aggregate) {
this.onDelete(aggregate);
// 刪除停止追蹤
this.detach(aggregate);
}
@Override
public void save(@NotNull T aggregate) {
// 如果沒有 ID,直接插入
if (aggregate.getId() == null) {
this.onInsert(aggregate);
this.attach(aggregate);
return;
}
// 做 Diff
EntityDiff diff = null;
try {
//aggregate = this.onSelect(aggregate.getId());
find(aggregate.getId());
diff = aggregateManager.detectChanges(aggregate);
} catch (IllegalAccessException e) {
//throw new RuntimeException("Failed to detect changes", e);
e.printStackTrace();
}
if (diff.isEmpty()) {
return;
}
// 調(diào)用 UPDATE
this.onUpdate(aggregate, diff);
// 最終將 DB 帶來的變化更新回 AggregateManager
aggregateManager.merge(aggregate);
}
/** 這幾個(gè)方法是繼承的子類應(yīng)該去實(shí)現(xiàn)的 */
protected abstract void onInsert(T aggregate);
protected abstract T onSelect(ID id);
protected abstract void onUpdate(T aggregate, EntityDiff diff);
protected abstract void onDelete(T aggregate);
}
OrderRepositoryDiffImpl 類
這個(gè)類繼承自 AggregateRepositorySupport 類,并實(shí)現(xiàn)具體的訂單存儲(chǔ)邏輯。
@Repository
@Slf4j
@Primary
public class OrderRepositoryDiffImpl extends AggregateRepositorySupport<Order, OrderId> implements OrderRepository {
//省略其他邏輯
@Override
protected void onUpdate(Order aggregate, EntityDiff diff) {
if (diff.isSelfModified()) {
OrderDO orderDO = orderConverter.toData(aggregate);
orderMapper.updateById(orderDO);
}
Diff orderItemsDiffs = diff.getDiff("orderItems");
if ( orderItemsDiffs instanceof ListDiff diffList) {
for (Diff itemDiff : diffList) {
if(itemDiff.getType() == DiffType.REMOVED){
OrderItem orderItem = (OrderItem) itemDiff.getOldValue();
orderItemMapper.deleteById(orderItem.getItemId().getValue());
}
if (itemDiff.getType() == DiffType.ADDED) {
OrderItem orderItem = (OrderItem) itemDiff.getNewValue();
orderItem.setOrderId(aggregate.getId());
OrderItemDO orderItemDO = orderItemConverter.toData(orderItem);
orderItemMapper.insert(orderItemDO);
}
if (itemDiff.getType() == DiffType.MODIFIED) {
OrderItem line = (OrderItem) itemDiff.getNewValue();
OrderItemDO orderItemDO = orderItemConverter.toData(line);
orderItemMapper.updateById(orderItemDO);
}
}
}
}
}
ThreadLocalAggregateManager 類
這個(gè)類主要通過ThreadLocal來保證在多線程環(huán)境下,每個(gè)線程都有自己的Entity上下文。
public class ThreadLocalAggregateManager<T extends Aggregate<ID>, ID extends Identifier<?>> implements AggregateManager<T, ID> {
private final ThreadLocal<DbContext<T, ID>> context;
private Class<? extends T> targetClass;
public ThreadLocalAggregateManager(Class<? extends T> targetClass) {
this.targetClass = targetClass;
this.context = ThreadLocal.withInitial(() -> new DbContext<>(targetClass));
}
@Override
public void attach(T aggregate) {
context.get().attach(aggregate);
}
@Override
public void attach(T aggregate, ID id) {
context.get().setId(aggregate, id);
context.get().attach(aggregate);
}
@Override
public void detach(T aggregate) {
context.get().detach(aggregate);
}
@Override
public T find(ID id) {
return context.get().find(id);
}
@Override
public EntityDiff detectChanges(T aggregate) throws IllegalAccessException {
return context.get().detectChanges(aggregate);
}
@Override
public void merge(T aggregate) {
context.get().merge(aggregate);
}
}
SnapshotUtils 類
SnapshotUtils 是一個(gè)工具類,它利用深拷貝技術(shù)來為對(duì)象創(chuàng)建快照。
public class SnapshotUtils {
@SuppressWarnings("unchecked")
public static <T extends Aggregate<?>> T snapshot(T aggregate)
throws IOException, ClassNotFoundException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(aggregate);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (T) ois.readObject();
}
}
這個(gè)類中的 snapshot 方法采用序列化和反序列化的方式來實(shí)現(xiàn)對(duì)象的深拷貝,從而為給定的對(duì)象創(chuàng)建一個(gè)獨(dú)立的副本。注意,為了使此方法工作,需要確保 Aggregate 類及其包含的所有對(duì)象都是可序列化的。
6. 小結(jié)
在本文中,我們深入探討了DDD(領(lǐng)域驅(qū)動(dòng)設(shè)計(jì))的一個(gè)核心構(gòu)件 —— 倉儲(chǔ)模式。借助快照模式和變更追蹤,我們成功解決了倉儲(chǔ)模式僅限于操作聚合根的約束,這為后續(xù)開發(fā)提供了一種實(shí)用的模式。
在互聯(lián)網(wǎng)上有豐富的DDD相關(guān)文章和討論,但值得注意的是,雖然許多項(xiàng)目宣稱使用Repository模式,但在實(shí)際實(shí)現(xiàn)上可能并未嚴(yán)格遵循DDD的關(guān)鍵設(shè)計(jì)原則。以訂單和訂單項(xiàng)為例,一些項(xiàng)目在正確地把訂單項(xiàng)作為訂單聚合的一部分時(shí),卻不合理地為訂單項(xiàng)單獨(dú)創(chuàng)建了Repository接口。而根據(jù)DDD的理念,應(yīng)當(dāng)僅為聚合根配備對(duì)應(yīng)的倉儲(chǔ)接口。通過今天的探討,我們應(yīng)該更加明確地理解和運(yùn)用DDD的原則,以確保更加健壯和清晰的代碼結(jié)構(gòu)。