界限上下文、领域

以订单系统设计为例

问题空间:

  • 下单(能不能买、怎么买);
  • 未完成支付的订单的支付、取消(要支付多少钱、不买了怎么办);
  • 已支付完成的订单的发货问题(什么仓库进行发货);
  • 发货订单的物流履约问题(仓库完成发货、物流签收、用户确认);

解决方案空间:领域

image

实体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);
 }
}

转载请注明出处
image


答案在风中
139 声望47 粉丝

程序员,先后供职于盛大、阿里巴巴、一条,目前在字节🐂🐴