简介
DDD设计实例
Domain-driven Design Example译文领域驱动设计示例 未读
源码实例:Domain-Driven Design领域驱动设计落地节选自《领域驱动设计第7章》假设我们正在为一家货运公司开发新的软件,最初的需求包括三项基本功能:
- 事先预约货物
- 跟踪客户货物的主要处理流程
- 当货物到达其处理过程中的某个位置时,自动向客户寄送发票
对应的博客 领域驱动设计DDD和CQRS落地 未读
cqrs
cqrs 全称command and Query Responsibility Segregation(隔离),也就是命令(增删改)与查询(查)职责分离。如果把Command 操作变成Event Sourcing,那么只需要记录不可修改的事件,并通过回溯事件得到数据的状态。
材料1
《软件架构设计》读写分离模型的典型特征:分别为读和写设计不同的数据结构。
材料2
阿里高级技术专家方法论:如何写复杂业务代码?一般来说实践DDD有两个过程:
- 套概念阶段:了解了一些DDD的概念,然后在代码中“使用”Aggregation Root,Bounded Context,Repository等等这些概念。更进一步,也会使用一定的分层策略。然而这种做法一般对复杂度的治理并没有多大作用。
- 融会贯通阶段:术语已经不再重要,理解DDD的本质是统一语言、边界划分和面向对象分析的方法。
你写的代码是别人的噩梦吗?从领域建模的必要性谈起软件的世界里没有银弹,是用事务脚本还是领域模型没有对错之分,关键看是否合适。就像自营和平台哪个模式更好?答案是都很好,所以亚马逊可以有三方入住,阿里也可以有自建仓嘛。实际上,CQRS就是对事务脚本和领域模型两种模式的综合,因为对于Query和报表的场景,使用领域模型往往会把简单的事情弄复杂,此时完全可以用奥卡姆剃刀把领域层剃掉,直接访问Infrastructure。
aggregate-framework
aggregate-framework 主要由公司大牛实现,所以通过分析它的源码来深入理解ddd和cqrs
领域驱动思想的 跟响应式编程 也有异曲同工之妙,剖析响应式编程的本质。将顺序式的命令编程 改为 事件/数据流。
示例使用
假设订单PricedOrder 有place和confirm 两个行为,其后续处理都是通过PricedOrder.apply(xxEvent)来进行的,dao类负责crud(这个没变)。service 只是负责 操作dao类以及(直接或间接)PricedOrder.apply(xxEvent) 方法。
- 订单的创建,触发了支付过程
- 支付过程
model 只是apply,剩下的交给eventHandler来做。这里有一个事件驱动的思想,即不是寻常的A==> B ;A ==> C;A ==>D,而是A 干完自己的活儿,发个事件就返回了。剩下的调用BCD,由eventbus 驱动执行。那么handler之间如何编组,如何揉和成一个统一的关系。那问题来了
- 后续处理是同步还是异步进行
- 一致性如何保证
- 业务处理如何接入
- 整个工作流程什么样
源码分析
domainObject Repository
在AggregateRoot接口里定义了一个方法apply,用来注册Domain Event。当调用Repository方法save来保存AggregateRoot时,将注册的Domain Event发布。
有三条线:
- 领域驱动
- 事务执行
- 异步执行
领域驱动
代码执行的核心是:
public interface EventBus {
public void subscribe(EventListener eventListener);
void publishInTransaction(EventMessage[] messages, LocalTransactionExecutor localTransactionExecutor);
}
从一个例子看执行流程:
public class OrderService {
@Transactional
public PricedOrder placeOrder(int productId, int price) {
PricedOrder pricedOrder = OrderFactory.buildOrder(productId, price);
return orderRepository.save(pricedOrder);
}
}
@Service
public class OrderHandler {
@EventHandler
public void handleOrderCreatedEvent(OrderPlacedEvent event) {
Payment payment = PaymentFactory.buildPayment(event.getPricedOrder().getId(),
String.format("p000%s", event.getPricedOrder().getId()), event.getPricedOrder().getTotalAmount());
paymentRepository.save(payment);
}
}
- 系统启动的时候,EventHandler 标记的类和方法会被封装为EventListener,加入到EventBus中
PricedOrder pricedOrder = OrderFactory.buildOrder(productId, price);
中执行了PricedOrder构造函数,执行了pricedOrder.apply(new OrderPlacedEvent(this));
本质上将pricedOrder 转换成了 EventMessage-
orderRepository.save(pricedOrder)
触发执行eventBus.publishInTransaction(EventMessage[] messages, LocalTransactionExecutor localTransactionExecutor);
然后各个eventhandler 就被触发执行了。当然,在spring Transaction场景下,eventBus.publishInTransaction也可以由事务调用触发。- 向threadlocal 挂一个clientSession
- 向clientSession 加入 AggregateEntry,AggregateEntry 聚合了pricedOrder 和一个全局的eventBus。同时挂一个实际的save逻辑
-
clientSession commit,主体就是执行 eventBus.publishInTransaction
- 因为eventBus是全局的,里面的EventListener太多, 所以要找到和EventMessage匹配的EventListener
- EventListener 根据 EventMessage 执行逻辑
- 执行save domain 逻辑本身
- clientSession flush、postHandle、closeClientSession 等完成后续流程
仅仅靠注解,如何知道OrderHandler.handleOrderCreatedEvent
处理的就是pricedOrder.apply(new OrderPlacedEvent(this));
?根据参数类型。这也是整个eventbus的意义所在:发布者发布事件,监听者监听事件。框架将整个过程整合在一起并处理。
“发布者发布事件,监听者监听事件”的优势在于:举一个例子, 笔者实现配置中心系统时,新增一个配置需要进行很多关联操作:
- 打掉系统中的缓存
- 新增ConfigChange数据
- 新增ChangeLog数据
并且随着业务需求的调整,新增一个配置要做的工作越来越多,并且在不断变化。新增配置如此,更改配置就更不用多说。后来,笔者提供了一个ConfigAddListener、ConfigChangeListener等。关心这些事件的人实现这个listener即可。
事务
@Transactional
public PricedOrder placeOrder(int productId, int price) {
PricedOrder pricedOrder = OrderFactory.buildOrder(productId, price);
return orderRepository.save(pricedOrder);
}
此处只是借用了spring-tx 的 Transactional 注解的调用接口做外壳,在其回调方法中塞的是compensable-transaction的事务处理逻辑。
异步执行
当eventhanlder 标记为异步任务时,将任务加入到Disruptor中。Disruptor是一个高性能队列,可以当做一个高性能的ArrayBlockingQueue 使用。然后有一个独立的Executor从Disruptor 取出任务并执行。
异步执行,带来几个问题
- 异步任务执行失败了怎么办?
- 异步离不开 队列,队列里的消息丢了(比如停电)怎么办
- 异步任务执行失败后重试,那重试好几次怎么办?
- 重试的时候,当时触发事件的 事件源本身就没有保存成功,或者状态改变怎么办?比如用户下了个订单,然后一个异步任务去发货,结果发货任务开始执行的时候用户把订单取消了。
所以在aggregate-framework 中,下单操作是以下逻辑
- 生成一个下单event
- event 保存到redis/db
- 订单数据库操作
- 发布 下单event
- 异步执行发货等任务
- 所有任务执行完了,删除redis/db 中的event
针对提出的几个问题
- 执行失败,便按照规定次数重试,若仍失败,等待手动处理(event一直在)
- 使用redis/db 等将event 持久化,并且先于订单存储操作,类似于mysql 写数据到磁盘之前先写日志
- eventhandler 必须支持幂等性
- eventhandler 执行之前,执行一个check检查函数,判断订单状态
eventhandler 不管由哪个实例产生, 可以由任意一个实例执行
缓存
聚合根 一次拉的数据太多,所以框架本身支持缓存, 也因此带来 缓存并发修改问题
小结
aggregate-framework 哪些部分是ddd,哪些是额外增强的
- eventhandler ,本身来自于事件驱动
- 事务支持,是为了一致性
- 异步执行,是为了效率
事件驱动大多数时候会用到异步,使用异步的话,一致性、异常处理、重启消息丢失等都来了。同步下不是问题的问题 都出现了,同步下是问题的,更复杂了。
和公司内大佬关于支付系统为何用ddd,以及ddd与微服务的关系(项目拆分角度)问题的讨论。
-
在做支付系统的时候,DDD提供了一个切分系统的思路,防止系统变成一个大煤球. 这个切分思想的好处是让工作经验比较浅的人做也不会出太大问题.通过事件发布机制把业务各环节串联起来,AGG提供的可靠机制保证不丢失数据
-
DDD是逻辑上的切分.微服务是实现上的切分。按DDD做模块切分,最终实现如果还是在一个应用里面,那就是单一应用程序.如果把DDD的模块分散成多个app,通过发布事件的方式建立联系协调工作,那就是微服务的实现