欢迎关注公众号(通过文章导读关注:【11来了】),及时收到 AI 前沿项目工具及新技术的推送!
在我后台回复 「资料」 可领取
编程高频电子书
!
在我后台回复「面试」可领取硬核面试笔记
!文章导读地址:点击查看文章导读!
感谢你的关注!
基于电商履约场景的 DDD 实战
基于 Cola 实现电商履约架构设计
我们在实际进行架构设计的时候,不用每一个方法都严格按照 Cola 或者 DDD 去设计,可以根据自己的需求以及习惯,做出一些合理的变动
这里将履约这个模块分为 6 个部分(后两个模块不重要,主要是前 4 个模块要清楚每一个模块的作用):
- api:与外界负责交互的模块
- application:原来的 app 层,负责全局服务的处理
- domain:领域层
- infrastructure:基础设施层
- rpc:与其他模块进行 rpc 通信的接口
- start:启动 SpringBoot 项目的模块
订单履约流程
订单履约的流程,当订单支付成功之后,订单上下文会发送一个【订单支付成功】的事件,在履约上下文中,就可以监听【订单支付成功】事件,之后开始履约的流程
- 首先会进入到 api 层
监听器放在 api 层,与外界进行交互,监听器收到事件之后,将外部的事件转为 Command,再调用 application 层提供的【应用服务】来进行处理
api --
@Component
@Slf4j
public class OrderPayedEventListener implements MessageListenerConcurrently {
@Autowired
private FulfillApplicationService fulfillApplicationService;
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
try {
for (MessageExt messageExt : list) {
String message = new String(messageExt.getBody());
log.info("OrderPayedEventListener message:{}", message);
// 1、将message转化为OrderPayedEvent领域事件
OrderPayedEvent orderPayedEvent = JSONObject.parseObject(message, OrderPayedEvent.class);
// 2、把领域事件,转换为一个command
OrderFulfillCommand orderFulfillCommand = buildCommand(orderPayedEvent);
// 3、交给app层的应用服务逻辑,去推动命令的流程编排和执行
fulfillApplicationService.executeOrderFulfill(orderFulfillCommand);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
} catch (Exception e) {
log.error("consumer error", e);
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
}
- application 层
在 application 层,就通过 Executor 执行履约的操作
@Component
public class FulfillApplicationService {
@Autowired
private OrderFulfillCommandExecutor orderFulfillCommandExecutor;
public void executeOrderFulfill(OrderFulfillCommand orderFulfillCommand) {
// 通过 Executor 来进行履约的处理
orderFulfillCommandExecutor.execute(orderFulfillCommand);
}
}
这里我们将 Executor 的具体实现也放在 application 层中去,在 Executor 中实现具体的履约流程:
-
首先是
保存订单
,这里先将 Command 给转成履约单 FulfillOrder,再将履约单进行保存操作,并且插入日志这里保存履约单的操作写到了 Gateway 中去,因为 Gateway 就是与下层 Infrastructure 层交互的
保存订单之后通过 【fulfillOrderLogDomainService】 来插入操作日志,这里将日志的插入操作写入到了 【fulfillOrderLogDomainService】 (即履约订单日志领域服务)中去,在这个 【DomainService】 中再调用 Gateway 去和 Infrastructure 进行交互,因为这里在插入日志信息的时候,还有一些额外的信息需要设置一下,所以在 DomainService 层进行了设置,之后再通过 Gateway 进行保存,可以保证 Gateway 层的职责比较单一(仅仅和 Infrastructure 交互或者其他模块交互)
-
其次是
风控拦截
,风控拦截已经是其他模块的功能了,毫无疑问使用 Gateway 来和其他上下文进行通信,这里在 Gateway 中就可以直接和风控模块暴露的 rpc 接口进行通信了(使用 Gateway 层解耦)如果该订单被风控拦截了,使用【domainEventGateway】来发布被拦截的领域事件
-
接下来是
预分仓
,由于预分仓这里,我们需要一些距离计算的方法来选择合适的仓库,而 Gateway 的目的是和其他模块进行交互,功能比较单一,因此预分仓这里我们再去创建一个【warehouseDomainService】即仓库的领域服务,在仓库的领域服务中再通过 Gateway 和仓库交互来获取所有仓库并且进行仓库的选择预分仓之后通过 【fulfillOrderLogDomainService】 来插入操作日志
-
下来是
分物流
,这里和预分仓是类似的,分物流中的操作也比较复杂,所以不能直接放到 Gateway 中去,要先创建一个【logisticsDomainService】物流的领域服务,在物流的领域服务中去执行分物流的一系列操作分物流之后通过 【fulfillOrderLogDomainService】 来插入操作日志
-
最后是
下库房
,先通过 Gateway 来和库房上下文进行交互,调用库房上下文暴露的接口来保存履约单下库房之后通过 【fulfillOrderLogDomainService】 来插入操作日志
@Component
@Slf4j
public class OrderFulfillCommandExecutor {
// ...
// 专门负责订单履约流程的编排,把这个流程按照战术建模的设计,完成落地开发
@Transactional(rollbackFor = Exception.class)
public void execute(OrderFulfillCommand orderFulfillCommand) {
// 第一步,保存订单,需要去使用履约订单仓储/gateway来进行保存
FulfillOrder fulfillOrder = fulfillOrderFactory.createFulfillOrder(orderFulfillCommand);
log.info("创建fulfillOrder实体:{}", JSONObject.toJSONString(fulfillOrder));
fulfillOrderGateway.save(fulfillOrder);
fulfillOrderLogDomainService.saveOrderCreatedLog(fulfillOrder);
// 第二步,风控拦截
Boolean interceptResult = riskControlApiGateway.riskControlIntercept(fulfillOrder);
log.info("风控拦截:{}", JSONObject.toJSONString(interceptResult));
if (!interceptResult) {
fulfillOrder.riskReject();
// 如果被风控拦截了,此时就需要发布订单被拦截的领域事件,通知人工审核
domainEventGateway.publishOrderInterceptedEvent(new OrderInterceptedEvent(fulfillOrder.getFulfillId().getFulfillId()));
return;
}else {
fulfillOrder.riskPass();
}
// 第三步,预分仓
Warehouse warehouse = warehouseDomainService.preAllocateWarehouse(fulfillOrder);
log.info("预分仓:{}", JSONObject.toJSONString(fulfillOrder.getFulfillOrderWarehouse().getWarehouseId()));
fulfillOrderLogDomainService.saveOrderWarehousedLog(fulfillOrder);
// 第四步,分物流
logisticsDomainService.allocateLogistics(fulfillOrder, warehouse);
fulfillOrderLogDomainService.saveOrderLogisticsLog(fulfillOrder);
log.info("分物流:{}",fulfillOrder.getLogisticsOrder().getLogisticsId());
// 第五步,下发库房
warehouseApiGateway.sendFulfillOrder(fulfillOrder, warehouse);
fulfillOrderLogDomainService.saveOrderInStoredLog(fulfillOrder);
log.info("下发库房");
}
}
上边的履约流程总共有 5 个步骤,这里只挑选两个具有代表性(Gateway 分别和 Infrastructure、其他模块交互的两种情况)的步骤说一下:
这里先说一下第一步:保存订单,在保存订单的时候,需要两步:
1、通过 【fulfillOrderGateway】 与 Infrastructure 交互,进行订单的保存
2、通过 【fulfillOrderLogDomainService】 进行日志的插入操作
这里在 Gateway 中直接调用了 Infrastructure 层的 DAO 进行数据库操作,整个流程就结束了
@Component
public class FulfillOrderGatewayImpl implements FulfillOrderGateway {
@Override
public void save(FulfillOrder fulfillOrder) {
FulfillOrderDO fulfillOrderDO = fulfillOrderDOConverter.convert(fulfillOrder);
List<FulfillOrderItemDO> fulfillOrderItemDOs = mapStructSupport.convertToFulfillOrderItemDOs(fulfillOrder.getFulfillOrderItems());
fulfillOrderDAO.save(fulfillOrderDO);
fulfillOrderItemDAO.saveBatch(fulfillOrderItemDOs);
}
}
接下来再看一下第二步:风控拦截,需要 2 步:
1、通过 【riskControlApiGateway】与风控模块进行交互,判断当前订单是否要被拦截
2、如果被拦截了,需要通过【domainEventGateway】发布【订单被拦截的事件】
这里在 Gateway 中直接就引入了风控模块的 API,通过 RPC 直接调用
@Component
public class RiskControlApiGatewayImpl implements RiskControlApiGateway {
@DubboReference(version = "1.0.0", retries = 0)
private RiskApi riskApi;
@Override
public Boolean riskControlIntercept(FulfillOrder fulfillOrder) {
// 通过 Converter 将 fulfillOrder 转为发往风控拦截的请求 FulfillOrderRiskRequest
FulfillOrderRiskRequest fulfillOrderRiskRequest = fulfillOrderRiskRequestConverter.convert(fulfillOrder);
// 调用风控拦截的 API 判断是否拦截
FulfillOrderRiskDTO fulfillOrderRiskDTO = riskApi.fulfillOrderRiskControlIntercept(fulfillOrderRiskRequest);
if(fulfillOrderRiskDTO == null) {
log.warn("风控调用失败");
return false;
}
if(!fulfillOrderRiskDTO.getRiskResult()) {
log.warn("风控检查拒绝通过");
return false;
}
return true;
}
}
上边代码比较多,可能看着不太清晰,这里我画张图:
这里还涉及一些实体对象的转换,监听器收到事件的时候,拿到的是消息,先将消息转为【OrderFulfillCommand】之后,再传入 Application 层,再将 【Command】转为【FulfillOrder】之后,传入 Domain 层,再转为【FulfillOrderDO】传入 Infrastructure 层
【OrderFulfillCommand】-> 【FulfillOrder】 使用 FulfillOrderFactory 来进行转换了,通过这个 Factory 创建一个履约单对象
【FulfillOrder】 ->【FulfillOrderDO】 使用 Converter 来进行转换,Converter 都放在了 Infrastructure 层
至此,订单履约流程的架构设计就完成了
那么可以发现,整个流程,从上到下,api -> application -> domain -> infrastructure,每个层的职责都是很清晰的,但是也正是因为职责划分的很清晰,从而导致代码在写起来的时候,需要比 MVC 架构多写很多的内容
因此对于中小型项目来说,盲目引入 DDD 只会导致项目的工作量剧增,DDD 的目的就是前期通过良好的分层、建模,后期可以降低维护成本