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















 
 
 










 
 
 
 