Aggregate(遵守不变性规则设计设计聚合边界,业务知识的内聚)
- 核心聚合设计
CQRS(读写隔离)
- ReadonlyPayingOrder用于解决支付环节内的读场景问题(构造相较于ReadWritePayingOrder更轻量),例如收银台获取待付款金额、支付结果页获取支付结果信息
- ReadWritePayingOrder响应PayingCommand、CancelCommand等事件,解决订单支付、取消相关问题
- 通过实体划分实现读写隔离,聚焦订单支付域内的问题(取消、支付等)
Domain Event(一致性边界之外的通信解决方案,跨域通信)
- 实体函数中变更自身数据同时生成相关的领域事件(数据变化和事件都伴随着实体,domain repository以及event handler分别处理数据持久化以及领域事件)
- 同时结合实体隔离达到事件隔离的目标,比如拼团订单支付完成的事件必须通过拼团订单实体来构造,能够从根源上避免事件污染问题;
/**
* 领域实体
*/
@DomainEntity
@Slf4j
@ToString
public abstract class AbstractReadWritePayingOrder extends AbstractPayingOrder implements IChangeTraceableAggregate<String, PayingOrderChangeInfo> {
/**
* 订单支付
*/
public void pay(OrderPayCommand orderPayCommand) {
if (OrderState.isCanceled(this.orderDTO.getState())) {
log.warn("order:{} is canceled! orderPayCommand:{}", orderPayCommand.getOrderNumber(), orderPayCommand);
} else if (OrderState.isPaid(this.orderDTO.getState())) {
//幂等
addDomainEvent(new OrderPayFinishedEvent(this));
} else if (OrderState.isInPaying(this.orderDTO.getState())) {
if (isPayFull()) {
//全额支付完成
//生成支付完成事件
addDomainEvent(new OrderPayFinishedEvent(this));
} else {
//部分支付完成...
}
}
//超额支付检测
if (isOverPaid()) {
addDomainEvent(new OverPaidEvent(this));
}
}
public void cancel(OrderCancelCommand orderCancelCommand) {
//...
}
}
/**
* 领域事件处理,跨域通信(可实现为接口调用、消息发送)
*/
@Slf4j
@Component
public class OrderPayFinishedEventHandler implements IDomainEventHandler<OrderPayFinishedEvent> {
@Autowired
private KafkaJsonMessageService kafkaJsonMessageService;
@Override
@Subscribe
public void handleDomainEvent(OrderPayFinishedEvent domainEvent) {
//订单整体支付完成
kafkaJsonMessageService.publish(QueueConfig.ORDER_PAY_FINISH_TOPIC, new Gson().toJson(domainEvent.getSimpleOrderInfo()));
}
}
Domain Repository(面向聚合、实体设计而非数据)
@Slf4j
@Repository
public class PayingOrderRepository extends RepositorySupport<AbstractReadWritePayingOrder, String, PayingOrderChangeInfo> {
/**
* 向上暴露聚合实体,隐藏聚合的数据获取方式
*/
@Override
public AbstractReadWritePayingOrder onFind(String orderNumber) {
return PayingOrderFactory.createPayingOrder(orderDTO, orderItemDTOs, totalPaidAmoint, productInfos);
}
public ReadonlyPayingOrder findReadonlyPayingOrder(String orderNumber) {
return PayingOrderFactory.createReadonlyPayingOrder(orderDTO, orderItemDTOs, totalPaidAmount);
}
/**
* 最小知识法则,通过变更追踪的技术方案构造稳定的持久化解决方案,从而不关心领域函数内具体变化的是什么字段,避免业务侵入
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void onUpdate(PayingOrderChangeInfo payingOrderChangeInfo) {
//订单更新
if (payingOrderChangeInfo.getUpdateOrderDTO() != null) {
orderRepository.updateByPrimaryKeySelective(payingOrderChangeInfo.getUpdateOrderDTO());
}
//操作日志保存
if (CollectionUtils.isNotEmpty(payingOrderChangeInfo.getInsertOrderChangeLog())) {
orderHistory.batchInsert(payingOrderChangeInfo.getInsertOrderChangeLog());
}
}
}
Snapshot方案实践:在聚合查询完成后,调用IChangeTraceableAggregate#attach函数建立数据快照;
答案在风中,公众号:答案在风中的BlogDDD实践落地(二)
Domain Service(领域对象的调度、低业务侵入)
- domain service 调度领域对象的职责,但不感知其具体实现细节
@Slf4j
@Service
public class PayingOrderDomainService {
@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);
AbstractReadWritePayingOrder payingOrder = redisLock.withr(() -> {
AbstractReadWritePayingOrder innerPayingOrder = orderPayCommand.isFreePay() ? payingOrderRepository.findOrderForFreePay(orderPayCommand) :
payingOrderRepository.find(orderPayCommand.getOrderNumber());
//订单支付
innerPayingOrder.pay(orderPayCommand);
//事务优先提交
transactionUtil.transaction(() -> payingOrderRepository.update(innerPayingOrder), Propagation.REQUIRES_NEW);
return innerPayingOrder;
}, () -> {
throw new ServiceRuntimeException(OrderReturnCode.SYSTEM_BUSY_CODE, orderPayCommand.toString());
});
domainEventBus.publish(payingOrder);
}
/**
* 取消订单
*/
public void cancelOrder(@NonNull OrderCancelCommand orderCancelCommand) {
RedisLock redisLock = mainRedisLockService.buildLock(ShippingOrderDomainService.LOCK_SCOPE_ORDER, orderCancelCommand.getOrderNumber(),
ShippingOrderDomainService.LOCK_DEFAULT_TTL);
AbstractReadWritePayingOrder payingOrder = redisLock
.withr(() -> {
AbstractReadWritePayingOrder innerPayingOrder = payingOrderRepository.find(orderCancelCommand.getOrderNumber());
//取消订单
innerPayingOrder.cancel(orderCancelCommand);
//持久化
transactionUtil.transaction(() -> payingOrderRepository.update(innerPayingOrder), Propagation.REQUIRES_NEW);
return innerPayingOrder;
}, () -> {
throw new ServiceRuntimeException(OrderReturnCode.SYSTEM_BUSY_CODE, orderCancelCommand.toString());
});
//发送领域事件
domainEventBus.publish(payingOrder);
}
}
Domain Test(聚焦实体,通过领域实体的职责解决复杂的业务场景构造问题)
测试过程
- 基本数据准备用于构造基准测试实体
- 通过基准测试实体的领域函数模拟领域生命周期内的不同状态
- 以不同的状态下的实体,验证聚合设计时的不变性规则以及相应的领域事件
收益
由于我们将业务都内聚到了领域实体内部(从上面可以看到service和repository中已经做到了无业务侵入,已不再是我们的测试重点),因此我们得以聚焦于实体测试。
以支付订单为例,我们按照不同类型的订单我们准备了11个不同的订单实体,并通过执行领域实体的pay()、cancel()或者模拟三方支付回调修改已支付金额来叠加影响(我们可以自由组合得到不同类型的不同状态的订单),以覆盖了近400个测试场景。
转载请注明出处
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。