大家好,我是飘渺。今日咱们持续更新DDD(范畴驱动规划) & 微服务系列。

在之前的文章中,咱们评论了如安在DDD中结构化应用程序。咱们了解到,在DDD中一般将应用程序分为四个层次,分别为用户接口层(Interface Layer),应用层(Application Layer),范畴层(Domain Layer),和根底设施层(Infrastructure Layer)。此外,在用户注册的主题中,咱们简要地提及了资源库形式。可是,那时咱们并没有深化评论。今日,我将为大家具体介绍资源库形式,这在DDD中是一个十分重要的概念。

1. 传统开发流程剖析

首要,让咱们回忆一下传统的以数据库为中心的开发流程。

在这种开发流程中,开发者一般会创立Data Access Object(DAO)来封装对数据库的操作。DAO的首要优势在于它可以简化构建SQL查询、办理数据库衔接和事务等底层使命。这使得开发者可以将更多的精力放在事务逻辑的编写上。可是,DAO尽管简化了操作,但仍然直接处理数据库和数据模型。

值得留意的是,Uncle Bob在《代码整齐之道》一书中,经过一些术语生动地描绘了这个问题。他将体系元素分为三类:

硬件(Hardware): 指那些一旦创立就不可(或难以)更改的元素。在开发布景下,数据库被视为“硬件”,由于一旦挑选了一种数据库,例如MySQL,转向另一种数据库,如MongoDB,一般会带来巨大的本钱和应战。

软件(Software): 指那些创立后可以随时修正的元素。开发者应该致力于使事务代码作为“软件”,由于事务需求和规矩总是在不断改动,因而代码也应该具有相应的灵敏性和可调整性。

固件(Firmware): 是那些与硬件紧密耦合,但具有必定的软性特色的软件。例如,路由器的固件或Android固件。它们为硬件供给笼统,但一般只适用于特定类型的硬件。

经过了解这些术语,咱们可以认识到数据库应视为“硬件”,而DAO在本质上归于“固件”。可是,咱们的方针是使咱们的代码坚持像“软件”那样的灵敏性。可是,当事务代码过于依赖于“固件”时,它会受到约束,变得难以更改。

让咱们经过一个具体的例子来进一步了解这个概念。下面是一个简略的代码片段,展现了一个方针怎么依赖于DAO(也便是依赖于数据库):

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);
    // 此处省掉许多事务逻辑
}

上面的代码片段看似无可厚非,但假设在未来咱们需求参加缓存逻辑,代码则需求改为如下:

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);
    }
    // 此处省掉许多事务逻辑
}

可以看到,刺进缓存逻辑后,本来简略的代码变得杂乱。本来一行代码现在至少需求三行。随着代码量的添加,假如你在某处忘掉检查缓存或忘掉更新缓存,可能会导致细微的功用下降或者更糟糕的是,缓存和数据库的数据不共同,然后导致bug。这种问题随着代码量和杂乱度的添加会变得愈加严峻,这便是软件被“固化”的结果。

因而,咱们需求一个规划形式来隔离咱们的软件(事务逻辑)与固件/硬件(DAO、数据库),以进步代码的强健性和可保护性。这个形式便是DDD中的资源库形式(Repository Pattern)。

2. 深化了解资源库形式

在DDD(范畴驱动规划)中,资源库起着至关重要的效果。资源库的中心使命是为应用程序供给共同的数据拜访进口。它答应咱们以一种与底层数据存储无关的办法,来存储和检索范畴方针。这对于将事务逻辑与数据拜访代码解耦对错常有价值的。

2.1 资源库形式在架构中的位置

资源库是一种广泛应用的架构形式。事实上,当你运用比方Hibernate、Mybatis这样的ORM结构时,你现已在间接地运用资源库形式了。资源库扮演着方针的供给者的人物,并且处理方针的耐久化。让咱们看一下耐久化:耐久化意味着将数据保存在一个耐久前言,比方联系型数据库或NoSQL数据库,这样即便应用程序中止,数据也不会丢掉。这些耐久化前言具有不同的特性和优点,因而,资源库的完成会依据所运用的前言有所不同。

资源库的规划一般包括两个首要组成部分:界说和完成界说部分是一个笼统接口,它只描绘了咱们可以对数据履行哪些操作,而不触及具体怎么履行它们。完成部分则是这些操作的具体完成。它依赖于一个特定的耐久化前言,并可能需求与特定的技能进行交互。

2.2 范畴层与根底设施层

依据DDD的分层架构,范畴层包括一切与事务范畴有关的元素,包括实体、值方针和聚合。范畴层表明事务的中心概念和逻辑。

另一方面,根底设施层包括支撑其他层的通用技能,比方数据库拜访、文件体系交互等。

资源库形式很好地适用于这种分层结构。资源库的界说部分,即笼统接口,坐落范畴层,由于它直接与范畴方针交互。而资源库的完成部分则归于根底设施层,它处理具体的数据拜访逻辑。

以DailyMart体系中的CustomerUser为例

DDD中的Repository模式,值得所有人掌握!

如上图所示,CustomerUserRepository是资源库接口,坐落范畴层,操作的方针是CustomerUser聚合根。CustomerUserRepositoryImpl是资源库的完成部分,坐落根底设施层。这个完成部分操作的是耐久化方针,这就需求在根底设施层中有一个组件来处理范畴方针与数据方针的转化,在之前的文章中现已引荐运用东西mapstruct来完成这种转化。

2.3 小结

资源库是DDD中一个强壮的概念,答应咱们以一种整齐和共同的办法来处理数据拜访。经过将资源库的界说放在范畴层,并将其完成放在根底设施层,咱们可以有效地将事务逻辑与数据拜访代码解耦,然后使应用程序愈加灵敏和可保护。

3. 仓储接口的规划准则

上文曾经讲过,传统Data Mapper(DAO)归于“固件”,和底层完成(DB、Cache、文件体系等)强绑定,假如直接运用会导致代码“固化”。所以为了在Repository的规划上体现出“软件”的特性,首要需求留意以下三点:

  1. 接口名称不应该运用底层完成的语法: 咱们常见的insert、select、update、delete都归于SQL语法,运用这几个词相当于和DB底层完成做了绑定。相反,咱们应该把 Repository 当成一个中性的类 似Collection 的接口,运用语法如 find、save、remove。在这儿特别需求指出的是区别 insert/add 和 update 本身也是一种和底层强绑定的逻辑,一些贮存如缓存实际上不存在insert和update的差异,在这个 case 里,运用中性的 save 接口,然后在具体完成上依据情况调用 DAO 的 insert 或 update 接口。

  2. 出参入参不应该运用底层数据格式: Repository接口坐落范畴层,底子看不到也不关心根底设施层的完成,当底层的存储技能改动时,范畴模型不需求做任何修正。仓储接口操作的方针是实体,实际上应该是聚合根(Aggregate Root)方针。

  3. 应该防止所谓的“通用”Repository形式: 许多 ORM 结构都供给一个“通用”的Repository接口,然后结构经过注解主动完成接口,比较典型的例子是Spring Data、Entity Framework等,这种结构的优点是在简略场景下很简略经过装备完成,可是害处是底子上无扩展的可能性(比方加定制缓存逻辑),在未来有可能仍是会被推翻重做。当然,这儿防止通用不代表不能有根底接口和通用的协助类,具体如下。

  4. 明晰的事务鸿沟:在大多数情况下,事务应该在应用服务层开端和完毕,而不是在仓储层。

当咱们规划仓储接口时,方针是发明一个明晰、可保护且松耦合的结构,这样可以让应用程序愈加灵敏和强健。以下是仓储接口规划的一些准则和最佳实践:

  1. 防止运用底层完成语法命名接口办法:仓储接口应该与底层数据存储完成坚持解耦。运用像insert, select, update, delete这样的词语,这些都是SQL语法,等于是将接口与数据库完成绑定。相反,应该视仓储为一个相似调集的笼统,运用更通用的词汇,如 findsaveremove。特别留意,区别insert/addupdate 本身便是与底层完成绑定的逻辑,有时候存储办法(如缓存)并不区别这两者。在这种情况下,运用一个中立的save接口,然后在具体的完成中依据需求调用insertupdate

  2. 运用范畴方针作为参数和返回值:仓储接口坐落范畴层,因而它不应该暴露底层数据存储的细节。当底层存储技能发生改动时,范畴模型应坚持不变。因而,仓储接口应以范畴方针,特别是聚合根(Aggregate Root)方针,作为参数和返回值。

  3. 防止过度通用化的仓储形式:尽管一些ORM结构(如Spring Data和Entity Framework)供给了高度通用的仓储接口,经过注解主动完成接口,但这种做法在简略场景下尽管方便,但一般缺少扩展性(例如,添加自界说缓存逻辑)。运用这种通用接口可能导致在未来的开发中遇到约束,乃至需求进行大的重构。但请留意,防止过度通用化并不意味着不能有底子的接口或通用的辅佐类。

  4. 界说明晰的事务鸿沟:一般,事务应该在应用服务层开端和完毕,而不是在仓储层。这样可以保证事务的范围明晰,并答应更好地控制事务的生命周期。

经过遵从上述准则和最佳实践,咱们可以创立一个仓储接口,不仅与底层数据存储解耦,还能支撑范畴模型的演变和应用程序的可保护性。

4. Repository的代码完成

在DailyMart项目中,为了完成DDD开发的最佳实践,咱们创立一个名为dailymart-ddd-spring-boot-starter的组件模块,专门寄存DDD相关的中心组件。这种做法简练地让其他模块经过引进此公共模块来遵从DDD准则。

DDD中的Repository模式,值得所有人掌握!

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> { }

这儿,聚合会完成Aggregate接口,而实领会完成Entity接口。聚合本质上是一种特别的实体,这种结构使逻辑愈加明晰。别的,咱们引进了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 创立通用Repository接口

接下来,咱们界说一个根底的Repository接口。

public interface Repository <T extends Aggregate<ID>, ID extends Identifier<?>> {
    T find(ID id);
    void remove(T aggregate);
    void save(T aggregate);
}

事务特定的接口可以在此根底上进行扩展。例如,对于订单,咱们可以添加计数和分页查询。

public interface OrderRepository extends Repository<Order, OrderId> {
	// 自界说Count接口,在这儿OrderQuery是一个自界说的DTO
    Long count(OrderQuery query);
    // 自界说分页查询接口
    Page<Order> query(OrderQuery query);
}

请留意,Repository的接口界说坐落Domain层,而具体的完成则坐落Infrastructure层。

4.3 实施Repository的底子功用

下面是一个简略的Repository完成示例。留意,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转化为Data Object(DO),然后运用Data Access Object(DAO)依据事务逻辑履行相应操作。在操作完成后,假如需求,还可以将DO转化回Entity。代码很简略,仅有需求留意的是save办法,需求依据Aggregate的ID是否存在且大于0来判别一个Aggregate是否需求更新仍是刺进。

4.4 Repository杂乱完成

处理单一实体的Repository完成一般较为直接,但当聚合中包括多个实体时,操作的杂乱性会添加。首要的问题在于,在单次操作中,并不是聚合中的一切实体都需求改动,而运用简略的完成会导致许多不用要的数据库操作。

以一个典型的场景为例:一个订单中包括多个产品明细。假如修正了某个产品明细的数量,这会一起影响主订单的总价,但对其他产品明细则没有影响。

DDD中的Repository模式,值得所有人掌握!

若选用根底的完成办法,会多出两个不用要的更新操作,如下所示:

@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());
        }
    }
}

在此示例中,会履行4个UPDATE操作,而实际上只需2个。一般情况下,这个额定的开支并不严峻,但假如非Aggregate Root的实体数量很大,这会导致很多不用要的写操作。

4.5 改动追寻(Change-Tracking)

针对上述问题,中心在于Repository接口的约束使得调用者只能操作Aggregate Root,而不能独自操作非Aggregate Root的实体。这与直接调用DAO的办法有明显差异。

一种处理计划是经过改动追寻才能来辨认哪些实体有改动,并且仅对这些改动过的实体履行操作。这样,从前需求手动判别的代码逻辑现在可以经过改动追寻来主动完成,让开发者真实只重视聚合的操作。曾经面的示例为例,经过改动追寻,体系可以判别出只要OrderItem2和Order发生了改动,因而只需求生成两个UPDATE操作。

改动追寻有两种干流完成办法:

  1. 依据快照Snapshot的计划: 数据从数据库提取后,在内存中保存一份快照,然后在将数据写回时与快照进行比较。Hibernate是选用此种办法的常见完成。

  2. 依据署理Proxy的计划: 当数据从数据库提取后,经过织入的办法为一切setter办法添加一个切面来检测setter是否被调用以及值是否发生改动。假如值发生改动,则将其符号为“脏”(Dirty)。在保存时,依据这个符号来判别是否需求更新。Entity Framework是一个选用此种办法的常见完成。

署理Proxy计划的优势是功用较高,几乎没有额定本钱,但缺陷是完成起来比较杂乱,并且当存在嵌套联系时,不简略检测到嵌套方针的改动(例如,子列表的添加和删除),可能会导致bug。

而快照Snapshot计划的优势是完成相对简略,本钱在于每次保存时履行全量比较(一般运用反射)以及保存快照的内存消耗。

由于署理Proxy计划的杂乱性,业界干流(包括EF Core)更倾向于运用依据Snapshot快照的计划。

此外,经过检测差异,咱们能辨认哪些字段发生了改动,并仅更新这些发生改动的字段,然后进一步降低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完成愈加高效和智能。这答应开发人员将留意力会集在事务逻辑上,而不用担心不用要的数据库操作。

DDD中的Repository模式,值得所有人掌握!

DDD中的Repository模式,值得所有人掌握!

5 在DailyMart中集成改动追寻

DailyMart体系内在盖了一个订单子域,该子域以Order作为聚合根,并将OrderItem归入为其子实体。两者之间构成一对多的联系。在对订单进行更新操作时,改动追寻显得尤为要害。

下面展现的是DailyMart体系中关于改动追寻的中心代码片段。值得留意的是,这些代码仅用于展现如安在库房形式中融入改动追寻,并非订单子域的完整完成。

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) {
      // 这儿的便是让查询出来的方针可以被追寻。
      // 假如自己完成了一个定制查询接口,要记住独自调用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 {
      //todo 从数据加载方针
      //aggregate = this.onSelect(aggregate.getId());
      find(aggregate.getId());
      diff = aggregateManager.detectChanges(aggregate);
    } catch (IllegalAccessException e) {
      //todo 优化 反常
      //throw new RuntimeException("Failed to detect changes", e);
      e.printStackTrace();
    }
    if (diff.isEmpty()) {
      return;
    }
    // 调用 UPDATE
    this.onUpdate(aggregate, diff);
    // 最终将 DB 带来的改动更新回 AggregateManager
    aggregateManager.merge(aggregate);
  }
  /** 这几个办法是继承的子类应该去完成的 */
  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 类,并完成具体的订单存储逻辑。

@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来保证在多线程环境下,每个线程都有自己的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 是一个东西类,它运用深复制技能来为方针创立快照。

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 办法选用序列化和反序列化的办法来完成方针的深复制,然后为给定的方针创立一个独立的副本。留意,为了使此办法作业,需求保证 Aggregate 类及其包括的一切方针都是可序列化的。

6. 小结

在本文中,咱们深化评论了DDD(范畴驱动规划)的一个中心构件 —— 仓储形式。凭借快照形式和改动追寻,咱们成功处理了仓储形式仅限于操作聚合根的约束,这为后续开发供给了一种实用的形式。

在互联网上有丰富的DDD相关文章和评论,但值得留意的是,尽管许多项目声称运用Repository形式,但在实际完成上可能并未严厉遵从DDD的要害规划准则。以订单和订单项为例,一些项目在正确地把订单项作为订单聚合的一部分时,却不合理地为订单项独自创立了Repository接口。而依据DDD的理念,应当仅为聚合根装备对应的仓储接口。经过今日的评论,咱们应该愈加明晰地了解和运用DDD的准则,以保证愈加强健和明晰的代码结构。

大家好,我是飘渺。今日咱们持续更新DDD(范畴驱动规划) & 微服务系列。

在之前的文章中,咱们评论了如安在DDD中结构化应用程序。咱们了解到,在DDD中一般将应用程序分为四个层次,分别为用户接口层(Interface Layer),应用层(Application Layer),范畴层(Domain Layer),和根底设施层(Infrastructure Layer)。此外,在用户注册的主题中,咱们简要地提及了资源库形式。可是,那时咱们并没有深化评论。今日,我将为大家具体介绍资源库形式,这在DDD中是一个十分重要的概念。

1. 传统开发流程剖析

首要,让咱们回忆一下传统的以数据库为中心的开发流程。

在这种开发流程中,开发者一般会创立Data Access Object(DAO)来封装对数据库的操作。DAO的首要优势在于它可以简化构建SQL查询、办理数据库衔接和事务等底层使命。这使得开发者可以将更多的精力放在事务逻辑的编写上。可是,DAO尽管简化了操作,但仍然直接处理数据库和数据模型。

值得留意的是,Uncle Bob在《代码整齐之道》一书中,经过一些术语生动地描绘了这个问题。他将体系元素分为三类:

硬件(Hardware): 指那些一旦创立就不可(或难以)更改的元素。在开发布景下,数据库被视为“硬件”,由于一旦挑选了一种数据库,例如MySQL,转向另一种数据库,如MongoDB,一般会带来巨大的本钱和应战。

软件(Software): 指那些创立后可以随时修正的元素。开发者应该致力于使事务代码作为“软件”,由于事务需求和规矩总是在不断改动,因而代码也应该具有相应的灵敏性和可调整性。

固件(Firmware): 是那些与硬件紧密耦合,但具有必定的软性特色的软件。例如,路由器的固件或Android固件。它们为硬件供给笼统,但一般只适用于特定类型的硬件。

经过了解这些术语,咱们可以认识到数据库应视为“硬件”,而DAO在本质上归于“固件”。可是,咱们的方针是使咱们的代码坚持像“软件”那样的灵敏性。可是,当事务代码过于依赖于“固件”时,它会受到约束,变得难以更改。

让咱们经过一个具体的例子来进一步了解这个概念。下面是一个简略的代码片段,展现了一个方针怎么依赖于DAO(也便是依赖于数据库):

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);
    // 此处省掉许多事务逻辑
}

上面的代码片段看似无可厚非,但假设在未来咱们需求参加缓存逻辑,代码则需求改为如下:

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);
    }
    // 此处省掉许多事务逻辑
}

可以看到,刺进缓存逻辑后,本来简略的代码变得杂乱。本来一行代码现在至少需求三行。随着代码量的添加,假如你在某处忘掉检查缓存或忘掉更新缓存,可能会导致细微的功用下降或者更糟糕的是,缓存和数据库的数据不共同,然后导致bug。这种问题随着代码量和杂乱度的添加会变得愈加严峻,这便是软件被“固化”的结果。

因而,咱们需求一个规划形式来隔离咱们的软件(事务逻辑)与固件/硬件(DAO、数据库),以进步代码的强健性和可保护性。这个形式便是DDD中的资源库形式(Repository Pattern)。

2. 深化了解资源库形式

在DDD(范畴驱动规划)中,资源库起着至关重要的效果。资源库的中心使命是为应用程序供给共同的数据拜访进口。它答应咱们以一种与底层数据存储无关的办法,来存储和检索范畴方针。这对于将事务逻辑与数据拜访代码解耦对错常有价值的。

2.1 资源库形式在架构中的位置

资源库是一种广泛应用的架构形式。事实上,当你运用比方Hibernate、Mybatis这样的ORM结构时,你现已在间接地运用资源库形式了。资源库扮演着方针的供给者的人物,并且处理方针的耐久化。让咱们看一下耐久化:耐久化意味着将数据保存在一个耐久前言,比方联系型数据库或NoSQL数据库,这样即便应用程序中止,数据也不会丢掉。这些耐久化前言具有不同的特性和优点,因而,资源库的完成会依据所运用的前言有所不同。

资源库的规划一般包括两个首要组成部分:界说和完成界说部分是一个笼统接口,它只描绘了咱们可以对数据履行哪些操作,而不触及具体怎么履行它们。完成部分则是这些操作的具体完成。它依赖于一个特定的耐久化前言,并可能需求与特定的技能进行交互。

2.2 范畴层与根底设施层

依据DDD的分层架构,范畴层包括一切与事务范畴有关的元素,包括实体、值方针和聚合。范畴层表明事务的中心概念和逻辑。

另一方面,根底设施层包括支撑其他层的通用技能,比方数据库拜访、文件体系交互等。

资源库形式很好地适用于这种分层结构。资源库的界说部分,即笼统接口,坐落范畴层,由于它直接与范畴方针交互。而资源库的完成部分则归于根底设施层,它处理具体的数据拜访逻辑。

以DailyMart体系中的CustomerUser为例

DDD中的Repository模式,值得所有人掌握!

如上图所示,CustomerUserRepository是资源库接口,坐落范畴层,操作的方针是CustomerUser聚合根。CustomerUserRepositoryImpl是资源库的完成部分,坐落根底设施层。这个完成部分操作的是耐久化方针,这就需求在根底设施层中有一个组件来处理范畴方针与数据方针的转化,在之前的文章中现已引荐运用东西mapstruct来完成这种转化。

2.3 小结

资源库是DDD中一个强壮的概念,答应咱们以一种整齐和共同的办法来处理数据拜访。经过将资源库的界说放在范畴层,并将其完成放在根底设施层,咱们可以有效地将事务逻辑与数据拜访代码解耦,然后使应用程序愈加灵敏和可保护。

3. 仓储接口的规划准则

上文曾经讲过,传统Data Mapper(DAO)归于“固件”,和底层完成(DB、Cache、文件体系等)强绑定,假如直接运用会导致代码“固化”。所以为了在Repository的规划上体现出“软件”的特性,首要需求留意以下三点:

  1. 接口名称不应该运用底层完成的语法: 咱们常见的insert、select、update、delete都归于SQL语法,运用这几个词相当于和DB底层完成做了绑定。相反,咱们应该把 Repository 当成一个中性的类 似Collection 的接口,运用语法如 find、save、remove。在这儿特别需求指出的是区别 insert/add 和 update 本身也是一种和底层强绑定的逻辑,一些贮存如缓存实际上不存在insert和update的差异,在这个 case 里,运用中性的 save 接口,然后在具体完成上依据情况调用 DAO 的 insert 或 update 接口。

  2. 出参入参不应该运用底层数据格式: Repository接口坐落范畴层,底子看不到也不关心根底设施层的完成,当底层的存储技能改动时,范畴模型不需求做任何修正。仓储接口操作的方针是实体,实际上应该是聚合根(Aggregate Root)方针。

  3. 应该防止所谓的“通用”Repository形式: 许多 ORM 结构都供给一个“通用”的Repository接口,然后结构经过注解主动完成接口,比较典型的例子是Spring Data、Entity Framework等,这种结构的优点是在简略场景下很简略经过装备完成,可是害处是底子上无扩展的可能性(比方加定制缓存逻辑),在未来有可能仍是会被推翻重做。当然,这儿防止通用不代表不能有根底接口和通用的协助类,具体如下。

  4. 明晰的事务鸿沟:在大多数情况下,事务应该在应用服务层开端和完毕,而不是在仓储层。

经过遵从上述准则和最佳实践,咱们可以创立一个仓储接口,不仅与底层数据存储解耦,还能支撑范畴模型的演变和应用程序的可保护性。

4. Repository的代码完成

在DailyMart项目中,为了完成DDD开发的最佳实践,咱们创立一个名为dailymart-ddd-spring-boot-starter的组件模块,专门寄存DDD相关的中心组件。这种做法简练地让其他模块经过引进此公共模块来遵从DDD准则。

DDD中的Repository模式,值得所有人掌握!

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> { }

这儿,聚合会完成Aggregate接口,而实领会完成Entity接口。聚合本质上是一种特别的实体,这种结构使逻辑愈加明晰。别的,咱们引进了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 创立通用Repository接口

接下来,咱们界说一个根底的Repository接口。

public interface Repository <T extends Aggregate<ID>, ID extends Identifier<?>> {
    T find(ID id);
    void remove(T aggregate);
    void save(T aggregate);
}

事务特定的接口可以在此根底上进行扩展。例如,对于订单,咱们可以添加计数和分页查询。

public interface OrderRepository extends Repository<Order, OrderId> {
	// 自界说Count接口,在这儿OrderQuery是一个自界说的DTO
    Long count(OrderQuery query);
    // 自界说分页查询接口
    Page<Order> query(OrderQuery query);
}

请留意,Repository的接口界说坐落Domain层,而具体的完成则坐落Infrastructure层。

4.3 实施Repository的底子功用

下面是一个简略的Repository完成示例。留意,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转化为Data Object(DO),然后运用Data Access Object(DAO)依据事务逻辑履行相应操作。在操作完成后,假如需求,还可以将DO转化回Entity。代码很简略,仅有需求留意的是save办法,需求依据Aggregate的ID是否存在且大于0来判别一个Aggregate是否需求更新仍是刺进。

4.4 Repository杂乱完成

处理单一实体的Repository完成一般较为直接,但当聚合中包括多个实体时,操作的杂乱性会添加。首要的问题在于,在单次操作中,并不是聚合中的一切实体都需求改动,而运用简略的完成会导致许多不用要的数据库操作。

以一个典型的场景为例:一个订单中包括多个产品明细。假如修正了某个产品明细的数量,这会一起影响主订单的总价,但对其他产品明细则没有影响。

DDD中的Repository模式,值得所有人掌握!

若选用根底的完成办法,会多出两个不用要的更新操作,如下所示:

@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());
        }
    }
}

在此示例中,会履行4个UPDATE操作,而实际上只需2个。一般情况下,这个额定的开支并不严峻,但假如非Aggregate Root的实体数量很大,这会导致很多不用要的写操作。

4.5 改动追寻(Change-Tracking)

针对上述问题,中心在于Repository接口的约束使得调用者只能操作Aggregate Root,而不能独自操作非Aggregate Root的实体。这与直接调用DAO的办法有明显差异。

一种处理计划是经过改动追寻才能来辨认哪些实体有改动,并且仅对这些改动过的实体履行操作。这样,从前需求手动判别的代码逻辑现在可以经过改动追寻来主动完成,让开发者真实只重视聚合的操作。曾经面的示例为例,经过改动追寻,体系可以判别出只要OrderItem2和Order发生了改动,因而只需求生成两个UPDATE操作。

改动追寻有两种干流完成办法:

  1. 依据快照Snapshot的计划: 数据从数据库提取后,在内存中保存一份快照,然后在将数据写回时与快照进行比较。Hibernate是选用此种办法的常见完成。

  2. 依据署理Proxy的计划: 当数据从数据库提取后,经过织入的办法为一切setter办法添加一个切面来检测setter是否被调用以及值是否发生改动。假如值发生改动,则将其符号为“脏”(Dirty)。在保存时,依据这个符号来判别是否需求更新。Entity Framework是一个选用此种办法的常见完成。

署理Proxy计划的优势是功用较高,几乎没有额定本钱,但缺陷是完成起来比较杂乱,并且当存在嵌套联系时,不简略检测到嵌套方针的改动(例如,子列表的添加和删除),可能会导致bug。

而快照Snapshot计划的优势是完成相对简略,本钱在于每次保存时履行全量比较(一般运用反射)以及保存快照的内存消耗。

由于署理Proxy计划的杂乱性,业界干流(包括EF Core)更倾向于运用依据Snapshot快照的计划。

此外,经过检测差异,咱们能辨认哪些字段发生了改动,并仅更新这些发生改动的字段,然后进一步降低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完成愈加高效和智能。这答应开发人员将留意力会集在事务逻辑上,而不用担心不用要的数据库操作。

DDD中的Repository模式,值得所有人掌握!

DDD中的Repository模式,值得所有人掌握!

5 在DailyMart中集成改动追寻

DailyMart体系内在盖了一个订单子域,该子域以Order作为聚合根,并将OrderItem归入为其子实体。两者之间构成一对多的联系。在对订单进行更新操作时,改动追寻显得尤为要害。

下面展现的是DailyMart体系中关于改动追寻的中心代码片段。值得留意的是,这些代码仅用于展现如安在库房形式中融入改动追寻,并非订单子域的完整完成。

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) {
      // 这儿的便是让查询出来的方针可以被追寻。
      // 假如自己完成了一个定制查询接口,要记住独自调用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 {
      //todo 从数据加载方针
      //aggregate = this.onSelect(aggregate.getId());
      find(aggregate.getId());
      diff = aggregateManager.detectChanges(aggregate);
    } catch (IllegalAccessException e) {
      //todo 优化 反常
      //throw new RuntimeException("Failed to detect changes", e);
      e.printStackTrace();
    }
    if (diff.isEmpty()) {
      return;
    }
    // 调用 UPDATE
    this.onUpdate(aggregate, diff);
    // 最终将 DB 带来的改动更新回 AggregateManager
    aggregateManager.merge(aggregate);
  }
  /** 这几个办法是继承的子类应该去完成的 */
  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 类,并完成具体的订单存储逻辑。

@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来保证在多线程环境下,每个线程都有自己的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 是一个东西类,它运用深复制技能来为方针创立快照。

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 办法选用序列化和反序列化的办法来完成方针的深复制,然后为给定的方针创立一个独立的副本。留意,为了使此办法作业,需求保证 Aggregate 类及其包括的一切方针都是可序列化的。

6. 小结

在本文中,咱们深化评论了DDD(范畴驱动规划)的一个中心构件 —— 仓储形式。凭借快照形式和改动追寻,咱们成功处理了仓储形式仅限于操作聚合根的约束,这为后续开发供给了一种实用的形式。

在互联网上有丰富的DDD相关文章和评论,但值得留意的是,尽管许多项目声称运用Repository形式,但在实际完成上可能并未严厉遵从DDD的要害规划准则。以订单和订单项为例,一些项目在正确地把订单项作为订单聚合的一部分时,却不合理地为订单项独自创立了Repository接口。而依据DDD的理念,应当仅为聚合根装备对应的仓储接口。经过今日的评论,咱们应该愈加明晰地了解和运用DDD的准则,以保证愈加强健和明晰的代码结构。

DDD&微服务系列源码现已上传至GitHub,假如需求获取源码地址,请重视公号 java日知录 并回复要害字 DDD 即可。