DDD架构学习

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放在一起,并没有像这样进行聚合

下面来解释一下常见概念:

  1. 领域对象: 凡是承载了业务含义、封装了业务逻辑的对象,都统称为领域对象。领域对象主要有下面这两种
  2. 实体: 实体是领域模型中最基础的对象。它的核心特征是拥有唯一的身份标识 比如订单,用户,商品就是实体,而实体中还有特殊的一种实体,被称为聚合根
  3. 聚合根: 聚合根本质上是一种特殊的实体。具有全局唯一的 ID、是外部访问聚合内部对象的唯一入口、负责维护内部数据的一致性。
  4. 值对象: 值对象没有唯一的身份标识,它存在的意义是描述实体的一些特征。它的核心特征是不可变性,且通过所有属性的组合来定义相等性 是实体中属性的值,以对象去代表其值
  5. 聚合:聚合是一组高度关联的领域对象(实体和值对象)的集合。 它最重要的作用是划定一个“一致性边界”
    在这个边界内部的所有对象,必须作为一个整体来保证业务规则和数据的一致性,其中一共限界上下文中可以包含多个聚合
  6. CQRS即 命令查询职责分离 我们在过去的mvc架构中,在开发过程中经常在写crud中用一个entity来进行读操作和写操作,经常出现将全部数据读出返回给前端,可能前端只需要其中的几列数据这种情况,或者出现前端和后端共同要求entity的情况 ,在并发的读情况下也会影响写性能,所以就有了CQRS ,在ddd中,cqrs体现在写端:严格遵循 DDD,使用聚合根(Aggregate Root)实体(Entity)来保证业务规则的完整性。读端:完全绕过复杂的聚合根,直接生成扁平化的 DTOVO 存入查询库。代码职责极度清晰:写代码的人只管业务对不对,读代码的人只管查得快不快。通过读操作和写操作 解决了下面这些问题 解决了“为了查数据,被迫加载整个聚合”的性能浪费问题 解决了“聚合根代码臃肿,被各种展示需求污染”的问题解决了“复杂报表查询,需要写一堆低效联表 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)

  • 执行特点:主线程发布事件后立刻返回(不阻塞)。监听器会在主事务成功提交后,被异步提交到线程池中执行。
  • 事务表现:监听器拥有独立的事务,且绝对保证了主业务的数据已经落库。
  • 适用场景:既要求主业务数据强一致,又要求高性能、低延迟的解耦操作。
  • 实战举例:电商下单成功后,需要调用第三方风控接口生成复杂的运营报表。这些操作既不能影响下单速度(需要异步),又必须建立在订单真实存在的基础上(需要事务提交后)。

这种情况就是目前最好用的

3 domain层

这是ddd架构中最重要的一层,我们具体的业务逻辑就在这里写

里面包含领域服务,领域对象这我们之前讲过了,但是这里还包含repository 这一层是干什么的呢?在我们的领

域服务和领域对象中,如果需要和dao层交互,根据洋葱架构的理念,是不能直接引入对应的组件的,这样会导致

耦合,后续改进和测试较麻烦,所以我们这里只定义接口,具体的实现放在infrastructure 去做

我们发现domain层还是有event ,这和application层的event有什么关系呢?domain层的event负责产生和存储

领域事件,而application层的service负责发送事件

我们看到domain层还有工厂的存在,Factory(工厂) 的核心职责非常单一:专门负责封装复杂领域对象(特别是聚合根 Aggregate Root)的创建过程。

当一个对象的创建过程非常复杂,或者需要确保对象一被创建出来就是“合法且完整”的状态时,我们就不能再简单地用 new 关键字去随便创建一个空对象然后挨个 set 属性了,这时候就需要工厂出马。

4 infrastructure

这是具体实现类存放的地方,上层不关心实现,只调用,对于rpc,mq消息发送,实体持久化,都是在domain层

定义接口,infrastructure层实现,由application层调用,需要注意的是,domain层的实体只关心自己的业务需

求,内置的方法也应该是和自己相关,对外是透明的,不应该在domain内持久化,发送rpc,mq消息,领域服务也

是一样的,只关心具体的业务,与业务无关的内容应该统统放进Application

posted @ 2026-05-06 23:57  折翼的小鸟先生  阅读(8)  评论(0)    收藏  举报