DDD架构学习
前言:学习ddd架构的记录
0 前置知识
下面是一个ddd的项目的架构
com.example.project
└── order (订单业务模块 / 限界上下文)├── interfaces (用户接口层)│ ├── controller // 接收 HTTP 请求,解析参数,返回响应│ ├── dto // 接口层的请求/响应对象 (Request/Response DTO)│ └── assembler // 负责 DTO 与领域对象/应用层对象的转换│├── application (应用层)│ ├── service // 应用服务,负责业务流程编排和事务控制│ ├── command // 命令对象 (CQRS中的C,代表写操作的入参)│ ├── query // 查询对象 (CQRS中的Q,代表读操作的入参)│ └── event // 应用层的事件处理或监听器│├── domain (领域层 - 核心业务逻辑)│ ├── model // 聚合根、实体 (Entity)、值对象 (Value Object)│ ├── repository // 仓储接口 (仅定义接口,不包含实现)│ ├── service // 领域服务 (处理跨实体的复杂业务逻辑)│ ├── event // 领域事件 (Domain Event,如 OrderCreatedEvent)│ └── factory // 工厂 (封装复杂聚合的创建逻辑)│└── infrastructure (基础设施层)├── persistence // 持久化实现│ ├── mapper // MyBatis/JPA 的 Mapper 接口│ ├── po // 持久化对象 (与数据库表一一对应,也叫 DO/Entity)│ └── repository // 仓储接口的实现类 (如 OrderRepositoryImpl)├── rpc // 调用外部微服务的 RPC Client 或防腐层 (ACL) 实现├── mq // 消息队列的生产者/消费者实现└── config // 该模块相关的技术配置
最大的区别就是我们的代码是根据域来组织的,像我们当前一个订单域下,需要的内容都在一起,但像我们传统的
mvc架构中,controller放在一起,service放在一起,mapper放在一起,并没有像这样进行聚合
下面来解释一下常见概念:
- 领域对象: 凡是承载了业务含义、封装了业务逻辑的对象,都统称为领域对象。领域对象主要有下面这两种
- 实体: 实体是领域模型中最基础的对象。它的核心特征是拥有唯一的身份标识 比如订单,用户,商品就是实体,而实体中还有特殊的一种实体,被称为聚合根
- 聚合根: 聚合根本质上是一种特殊的实体。具有全局唯一的 ID、是外部访问聚合内部对象的唯一入口、负责维护内部数据的一致性。
- 值对象: 值对象没有唯一的身份标识,它存在的意义是描述实体的一些特征。它的核心特征是不可变性,且通过所有属性的组合来定义相等性 是实体中属性的值,以对象去代表其值
- 聚合:聚合是一组高度关联的领域对象(实体和值对象)的集合。 它最重要的作用是划定一个“一致性边界”。
在这个边界内部的所有对象,必须作为一个整体来保证业务规则和数据的一致性,其中一共限界上下文中可以包含多个聚合 - CQRS即 命令查询职责分离 我们在过去的mvc架构中,在开发过程中经常在写crud中用一个entity来进行读操作和写操作,经常出现将全部数据读出返回给前端,可能前端只需要其中的几列数据这种情况,或者出现前端和后端共同要求entity的情况 ,在并发的读情况下也会影响写性能,所以就有了CQRS ,在ddd中,cqrs体现在写端:严格遵循 DDD,使用聚合根(Aggregate Root)和实体(Entity)来保证业务规则的完整性。读端:完全绕过复杂的聚合根,直接生成扁平化的 DTO 或 VO 存入查询库。代码职责极度清晰:写代码的人只管业务对不对,读代码的人只管查得快不快。通过读操作和写操作 解决了下面这些问题 解决了“为了查数据,被迫加载整个聚合”的性能浪费问题 解决了“聚合根代码臃肿,被各种展示需求污染”的问题解决了“复杂报表查询,需要写一堆低效联表 SQL”的问题
1. interfaces
相比我们三层架构中单独由controller 来接收请求,在ddd结构中使用interfaces 也就是用户接口层统一去做请求
接收与处理,在controller中绝对不能写业务逻辑,真正的业务逻辑由领域层中领域模型的方法和领域服务的方法
来执行,在interfaces层中,除了controller,还包含了当前接收请求需要用到的dto 和 转化用的assembler
dto很好理解,直接来接收前端传来的数据,我们放在dto里等待后续使用,在ddd架构中,有这样的一个要求
绝对不允许把“领域对象(聚合根/实体)”直接暴露给外部系统(比如直接当成 JSON 返回给前端)
所以我们直接将dto发送给后续的application的service是不行的,这里就需要用到assembler来进行转换,在收到
消息时,将dto 转换为领域对象 ,然后将该对象传入后续的 application 在application处理完后,也不能直接将
领域对象返回给前端,也是需要通过assembler来将其转化dto来返回给前端
内部其实只是进行了值拷贝罢了
2.application
application起到的作用更像是一个组织者,我们在接收到接口层的请求后,在application层中具体的service方法
中组织调用domain中的实体内部的方法和领域服务,注意此时应用层的service类似于我们mvc中的service,也是
一个接口,依赖于其实现类,在实现类内也不写业务逻辑,主要干两件事一是业务流程编排,也就是调用domain
的实体方法和领域服务,最多涉及一点格式转换,二是负责事务控制,来开启事务和提交事务
我们这里用到了CQRS的思想做了命令查询职责分离,我们看到aplication层下还有command和query ,这两部分
就是定义的数据盒子,用于在接口层使用assembler来根据读或者写将其转为query和command对象,读写走不
同的逻辑,不同的操作,比如读操作可以绕过聚合根,直接读数据库,可以不读原始数据库,可以去直接读es或
者构建好的宽表,绕过联表查询和拼接,便于提高效率和后续拓展,我们看到application层还有event ,这个
event用于事件监听,在其中可以写事件监听器,当我们在应用的主业务完成后,需要一些副业务时,就会发送一
个领域事件,这个事件被对应的事件监听器监听到了之后就会执行,事件执行分为下面四种
1. 同步非事务事件(默认) :
此时当事件发送后,主业务会出现阻塞,然后由当前主线程去执行触发的事件,执行完事件后回到主业务继续执行,监听器和发布者在同一个事务中。如果监听器里抛出了异常,整个事务会回滚。
2.异步事件
通过在监听器方法上添加 @Async 注解,并配合全局的 @EnableAsync 开启异步支持。
- 执行特点:发布者发布事件后立刻返回,不等待监听器执行。监听器会被提交到独立的线程池中去运行。
- 事务表现:监听器运行在全新的线程中,拥有独立的事务上下文,与原发布者的事务完全隔离。
- 适用场景:耗时较长、非核心业务、允许有短暂延迟的场景。
- 实战举例:用户下单成功后,发送短信/邮件通知、推送大数据埋点。这些操作不影响下单主流程,异步执行可以极大地提升接口的响应速度。
3. 同步事务事件 (@TransactionalEventListener)
这是解决“事务未提交,事件就被消费”这一经典 Bug 的神器。使用 @TransactionalEventListener 替代 @EventListener。
-
执行特点:依然是同步执行(阻塞主线程),但它的触发时机与数据库事务的生命周期绑定。
-
事务表现
:它可以根据
phase属性,精准控制在事务的哪个阶段执行。
AFTER_COMMIT(最常用):只有当主业务的事务成功提交后,监听器才会执行。AFTER_ROLLBACK:仅在事务回滚后执行。BEFORE_COMMIT:在事务提交前执行(可用于最终校验)。
-
适用场景:业务逻辑依赖主事务必须成功,且需要实时执行。
-
实战举例:用户下单成功后,需要更新 Redis 缓存中的商品库存。你必须确保订单已经稳稳地存入数据库(事务提交)后,才能去改缓存。如果用普通同步事件,万一事件执行了但主事务因为其他原因回滚了,缓存和数据库的数据就不一致了。
需要注意,对于默认的事务传播方式来说,此时主业务和副业务是事务不隔离的,虽然可以设置为主业务提交后监
听器执行,但是此时数据落库了,事务上下文未结束,还是处于一个事务,如果副业务报错,主业务会报错但数据已经落库了,就会导致不一致,如果想要避免这种情况就需要开启
propagation = Propagation.REQUIRES_NEW
4. 异步事务事件 (@Async + @TransactionalEventListener)
这是生产环境中最完美的组合拳,将上面两种模式结合:同时加上 @Async 和 @TransactionalEventListener(phase = AFTER_COMMIT)。
- 执行特点:主线程发布事件后立刻返回(不阻塞)。监听器会在主事务成功提交后,被异步提交到线程池中执行。
- 事务表现:监听器拥有独立的事务,且绝对保证了主业务的数据已经落库。
- 适用场景:既要求主业务数据强一致,又要求高性能、低延迟的解耦操作。
- 实战举例:电商下单成功后,需要调用第三方风控接口或生成复杂的运营报表。这些操作既不能影响下单速度(需要异步),又必须建立在订单真实存在的基础上(需要事务提交后)。
这种情况就是目前最好用的
