关于java:DDD实践落地二

8次阅读

共计 8829 个字符,预计需要花费 23 分钟才能阅读完成。

界线高低 文、畛域

以订单零碎设计为例

问题空间:

  • 下单(能不能买、怎么买);

  • 未实现领取的订单的领取、勾销(要领取多少钱、不买了怎么办);

  • 已领取实现的订单的发货问题(什么仓库进行发货);

  • 发货订单的物流履约问题(仓库实现发货、物流签收、用户确认);

解决方案空间:畛域

实体 Entity

特色:具备惟一的标识、可变性

  • 惟一标识:每个实体都具备惟一的标识,能够来自于数据库的 ID 或者是具备业务含意的复合标识,能够用于辨别不同实体;

  • 可变性:实体有显著的生命周期,在生命周期内,响应内部事件履行职责(解决方案),驱动自身的数据或状态的变动;

作用:落地解决方案

  • 实体的可变性取决于其具备的角色和职责,通过实体咱们能够内聚以后域内的业务逻辑,联合畛域实体资源库、畛域事件来长久化以及向其余域公布变更;

值对象 ValueObject

特色:无唯一性标识、不可变、无副作用(无状态)

作用:实体或聚合的隶属属性

聚合 Aggregate

将实体和值对象在一致性边界内组成聚合:

  • 在一致性边界内,实体数据具备强一致性(在这个边界内的行为都是原子的);

  • 正确的聚合设计特色是事务肯定是清晰的;

  • 防止臃肿聚合:将一个聚合切分为多个聚合甚至是是不同域;

准则:

  • 在一致性边界内建模真正的不变条件(比方父单拆单实现意味着子单肯定存在),这种不变条件同样是对畛域测试的重点;

  • 设计小聚合,臃肿的聚合是贫血模型的另一个极其,对性能、可测试性、隔离性、长期保护都是有侵害的;

  • 通过惟一标识援用其余聚合;

    
    咱们在议论聚合的一致性边界,如果聚合间接援用其余聚合及其容易让研发人员在具体开发过程中突破这一准则(比方在拍品这个聚合内间接操作拍卖流动这个聚合缩短流动完结工夫,这会导致不同的拍品所持有的流动聚合的不一致性);
  • 在边界之外应用最终一致性;

    
    采纳畛域事件的形式,向其余域公布变更,而不影响以后聚合边界内的事务提交;

总结:

  • 以后聚合变动强一致性;跨聚合变动,每个聚合外部谋求强一致性,不同聚合间谋求最终一致性;

  • 聚合 (Aggregate) 的职责执行,能够总结为在业务规定的束缚下,原子的扭转本身数据(DomainRepository),同时将本身的变动传递给其余关怀该变动的聚合(DomainEvent);

资源库 DomainRepository

作用:一致性边界内的数据变动的长久化

查问聚合:

  • 不同于 DAO,DAO 面向的是数据,聚合查问面向的是实体(可能起源自多种存储实现下的数据);

  • 聚合的查问须要向应用聚合的中央暗藏,聚合查找的细节,这种分层也有助于咱们在聚合查问的中央建设数据变动的追踪或者通过惟一标识查问聚合的缓存优化;

长久化聚合:

稳固长久化的意义:

  • 面向数据的编程形式下,在业务规定变动过程中咱们很难始终确保边界内的一致性不被毁坏(这种毁坏甚至是有意识的,因为对数据的操作被扩散在各种业务代码中)。

  • 借助聚合咱们失去了实体层面的一致性保障,DomainRepository 须要做的就是将聚合内的不同变动以一种稳固的形式进行长久化。

    
    这个过程中咱们须要警觉业务知识被扩散到长久化层,同时只有不变的长久层实现能力开释测试人员的精力,让他们聚焦于实体测试。咱们通过建设对实体变动的追踪(Change-Tracking),并且将变动的数据后果反馈至对应的存储层进行存储。

Change-Tracking 计划:

基于 Snapshot 的计划:当数据从 DB 里取出来后,在内存中保留一份 snapshot,而后在数据写入时和 snapshot 比拟。

基于 Proxy 的计划:当数据从 DB 里取出来后,通过 weaving 的形式将所有 setter 都减少一个切面来判断 setter 是否被调用以及值是否变更,如果变更则标记为 Dirty。在保留时依据 Dirty 判断是否须要更新。

殷浩,公众号:技术琐话阿里技术专家详解 DDD 系列 第三讲 – Repository 模式

Snapshot 计划实际:

  1. 在聚合查问实现后,调用 IChangeTraceableAggregate#attach 函数建设数据快照;

// 聚合实现 IChangeTraceableAggregate 对应的 attach/detach 办法
@DomainEntity
@Slf4j
@ToString
public abstract class AbstractPayingOrder implements IChangeTraceableAggregate<String, PayingOrderChangeInfo> {
 // 订单信息
 @Getter
 protected NewOrderDTOWithBLOBs orderDTO;
 // 通过自定义 mybatis-generator-plugin 插件生成数据层 diff 工具;protected transient NewOrderDTOWithBLOBs.Tracer orderDTOTracer;
 public AbstractPayingOrder(@NonNull NewOrderDTOWithBLOBs orderDTO) {
 this.orderDTO = orderDTO;
 // ...
 }
 @Override
 public void attach() {
 // 追踪 orderDTO 变更
 this.orderDTOTracer = this.orderDTO.new Tracer();
 this.orderDTOTracer.attach();}
 @Override
 public void detach() {if (this.orderDTOTracer != null) {this.orderDTOTracer.detach();
 }
 }
}
//DomainRepository 的查问聚合的函数执行后回调 IChangeTraceableAggregate#attach 建设 snapshot
public abstract class RepositorySupport<A extends IChangeTraceableAggregate<ID, D>, ID extends Serializable, D> implements IRepository<A, ID> {
 // find 回调
 protected abstract A onFind(ID id);
 @Override
 public A find(@NonNull ID id) {A aggregate = this.onFind(id);
 if (aggregate != null) {
 // 这里的就是让查问进去的对象可能被追踪。// 如果本人实现了一个定制查问接口,要记得独自调用 attach。aggregate.attach();}
 return aggregate;
 }
 //...
}

2. 聚合职责函数执行实现后,DomainRepository 基于 IChangeTraceableAggregate#getChangeInfo()后果构建无业务侵入的长久化逻辑

// 聚合实现 IChangeTraceableAggregate 的获取增量变动的办法
@DomainEntity
@Slf4j
@ToString
public abstract class PayingOrder implements IChangeTraceableAggregate<String, PayingOrderChangeInfo> {
 @Getter
 protected NewOrderDTOWithBLOBs orderDTO;
 protected transient NewOrderDTOWithBLOBs.Tracer orderDTOTracer;
 public AbstractPayingOrder(@NonNull NewOrderDTOWithBLOBs orderDTO) {
 this.orderDTO = orderDTO;
 // ...
 }
 //...
 @Override
 public PayingOrderChangeInfo getChangeInfo() {
 // 调用 diff 工具返回增量变动,参看:https://gitlab.yit.com/backend/mybatis-generator-plugin/blob/master/src/main/java/com/yit/mybatisplugin/TracerPlugin.java
 NewOrderDTOWithBLOBs orderDTOWithBLOBs = this.orderDTOTracer.getChangeInfo();
 if (orderDTOWithBLOBs != null) {orderDTOWithBLOBs.setUpdatedAt(new Date());
 }
 return new PayingOrderChangeInfo(orderDTOWithBLOBs);
 }
}
//DomainRepository 的 update 函数回调 IChangeTraceableAggregate#getChangeInfo 获取增量变动
public abstract class RepositorySupport<A extends IChangeTraceableAggregate<ID, D>, ID extends Serializable, D> implements IRepository<A, ID> {
 @Override
 public void update(@NonNull A aggregate) {
 // 调用 UPDATE
 this.onUpdate(aggregate.getChangeInfo());
 // 从新追踪(清理旧快照,新建快照)aggregate.detach();
 aggregate.attach();}
}
@Component
public class PayingOrderRepository extends RepositorySupport<AbstractPayingOrder, String, PayingOrderChangeInfo> {
 @Override
 protected AbstractPayingOrder onFind(String orderNumber) {//...}
 @Override
 protected void onUpdate(PayingOrderChangeInfo payingOrderChangeInfo) {
 // 长久化层不再关怀不同业务场景内变动的具体数据字段
 if (payingOrderChangeInfo.getUpdateOrderDTO() != null) {orderRepository.updateByPrimaryKeySelective(payingOrderChangeInfo.getUpdateOrderDTO());
 }
 }
}

畛域事件

作用:一致性边界之外的不同聚合间的变动感知

留神点:畛域事件自身是值对象,所以切勿在事件中公布畛域对象;

事件模型实际:

基于 eventBus 的畛域事件公布器

// 实体中生成畛域事件:public abstract class PayingOrder implements IChangeTraceableAggregate<String, PayingOrderChangeInfo> {public void pay(OrderPayCommand orderPayCommand){
 // 以后聚合数据变更
 this.orderDTO.setState(OrderStateEnum.PAY_FINISH.getValue());
 // 生成畛域事件
 addDomainEvent(new OrderPayFinishedEvent(this));
 }
}
// 长久化实现后公布事件:public class OrderDomainService {public void confirmOrder(String orderNumber){ShippingOrder shippingOrder = shippingOrderRepository.find(orderNumber);
 shippingOrder.confirm();
 // 长久化
 shippingOrderRepository.update(shippingOrder);
 // 公布畛域事件
 domainEventBus.publish(shippingOrder);
 }
}
// 事件监听
@Slf4j
@Component
public class OrderPayFinishedEventHandler implements IDomainEventHandler<OrderPayFinishedEvent> {
 @Override
 @Subscribe
 public void handleDomainEvent(OrderPayFinishedEvent domainEvent) {//...}
}

畛域服务

现实中的畛域服务代码模板:

业务逻辑被内聚在畛域实体内,在畛域服务中调度聚合资源库进行长久化,通过事件模型公布在聚合中生成的畛域事件。

@Service
public class DomainService{ 
 @Autowired
 private DomainRepository domainRepository;
 @Autowired
 private DomainEventBus domainEventBus;
 public void action(Identity identity, Command command) {
 //1. 锁定畛域实体
 if(trylock(identity)){
 //2. 查问聚合
 DomainEntity domainEntity = domainRepository.find(identity);
 //3. 执行聚合操作
 domainEntity.action(command);
 //4. 长久化聚合变动,独立事务提交
 domainRepository.update(domainEntity);
 //5. 公布畛域事件
 domainEventBus.publish(domainEntity);
 }
 }
}

畛域服务实际:

@Service
public class PayingOrderPayDomainService {
 @Autowired
 private PayingOrderRepository payingOrderRepository;
 @Autowired
 private DomainEventBus domainEventBus;
 @Autowired
 private RedisLockService mainRedisLockService;
 @Autowired
 private TransactionUtil transactionUtil;
 public void pay(OrderPayCommand orderPayCommand) {
 // 基于实体标识的分布式锁
 RedisLock redisLock = mainRedisLockService.buildLock(ShippingOrderDomainService.LOCK_SCOPE_ORDER, orderPayCommand.getOrderNumber(),
 ShippingOrderDomainService.LOCK_DEFAULT_TTL);
 AbstractPayingOrder payingOrder = redisLock.with(() -> {AbstractPayingOrder innerPayingOrder = payingOrderRepository.find(orderPayCommand.getOrderNumber());
 // 订单领取
 innerPayingOrder.pay(orderPayCommand);
 // 事务提交
 transactionUtil.transaction(() -> payingOrderRepository.update(innerPayingOrder), Propagation.REQUIRES_NEW);
 return innerPayingOrder;
 }, () -> {throw new ServiceRuntimeException(OrderReturnCode.SYSTEM_BUSY_CODE);
 });
 // 公布畛域事件
 domainEventBus.publish(payingOrder);
 }
}

DDD 测试

** 准则
**

  • 聚焦实体,实体是业务变动的焦点,长久层、事件处理尽可能的轻或者稳固;

  • 检测聚合的不变性规定;

  • 检测过程中公布的畛域事件;

畛域测试实际

public class ShippingOrderTest{
 @DataProvider
 public Iterator<Object> provideTestData() {List<Object> params = new ArrayList<>();
 params.add(new DeliverySubOrderItemsSuccessParam("子订单未发货,一个子订单只有一个 item,子单商品待发货", OrderStateEnum.PROCESSING.getValue(),
 OrderStatus.WAIT_SEND, OrderStatus.WAIT_RECEIVE, false,
 new ArrayList<SubOrderItemParam>() {{add(new SubOrderItemParam(checkItemEntityId, 124478, OrderSkuLogisticsStatus.INIT, OrderSkuLogisticsStatus.ON_THE_WAY));
 }}, new String[] {"SubOrderItemDeliveredEvent", "SubOrderDeliveredEvent"}));
 //...
 return params.iterator();}
 @Test(description = "子订单商品发货测试", dataProvider = "provideTestData")
 public void testDeliverySubOrderItems(DeliverySubOrderItemsSuccessParam param) {
 //1、测试数据筹备
 NewOrderDTOWithBLOBs orderDTO = new NewOrderDTOWithBLOBsBuilder().build();
 List<NewSubOrderDTO> newSubOrderDTOList = Lists.newArrayList(new NewSubOrderDTOBuilder().subOrderStatus(param.subOrderStatus).subOrderState(param.subOrderState).build());
 DeliverItemsCommand deliveryItemsCommand = new DeliveryItemsCommandBuilder().resetDeliveryInfo(param.resetDeliveryInfo).build();
 //2 初始化 ShippingOrder 实体
 ShippingOrder shippingOrder = new ShippingOrder(orderDTO, newSubOrderDTOList);
 // 建设追踪
 shippingOrder.attach();
 //3 执行待测办法
 shippingOrder.deliverSubOrderItems(deliveryItemsCommand);
 //4 通过 changeInfo 验证后果
 ShippingOrderChangeInfo shippingOrderChangeInfo = shippingOrder.getChangeInfo();
 //4.1 验证 suborderchange
 SubOrderItemParam subOrderItemParam = param.subOrderItemParams.stream().filter(p -> p.id == checkItemEntityId).collect(Collectors.toList()).get(0);
 if(subOrderItemParam.subOrderItemStatus != OrderSkuLogisticsStatus.COMPLETED) {NewSubOrderItemDTO newSubOrderItemDTO = shippingOrderChangeInfo.getUpdateSubOrderItemDTOList().get(0);
 assertThat(newSubOrderItemDTO.getYitiaoLogisticsCreatedAt()).as("运单号更新工夫近 1 秒钟").isAfter(DateTime.now().plusSeconds(-2).toDate());
 } else {assertThat(shippingOrderChangeInfo.getUpdateSubOrderItemDTOList().size() == 0).as("子订单商品没有更新").isTrue();}
 //5 验证畛域事件列表
 List<String> domainEventNames = shippingOrder.getDomainEvents().stream().map(p -> p.getClass().getName().replaceAll(".*.", "")).collect(Collectors.toList());
 assertThat(domainEventNames).as("畛域事件").containsOnly(param.expectDomainEventNames);
 }
}

转载请注明出处

正文完
 0