一个只会 MVC 的 java 小白对 DDD 的理解

四层架构

而不是 MVC 的三层

领域驱动设计(DDD)中,并没有一个所谓的"唯一标准"架构,但行业内公认的经典架构 是 Eric Evans 在原著中提出的 四层分层架构

层级名称 职责描述 核心组件
用户界面层 (Interfaces) 负责与外部交互。处理 HTTP 请求、解析 JSON、返回数据给前端。 Controller, WebSocket, DTO 映射器
应用层 (Application) 不含业务逻辑。负责编排业务流程(比如先查数据库,再调接口,最后发消息)。 Application Services, Command/Query
领域层 (Domain) 系统的灵魂。纯粹的业务逻辑。不依赖数据库、不依赖第三方。 Entity, Value Object, Aggregate, Domain Service
基础设施层 (Infrastructure) 提供技术支撑。数据库持久化、缓存、发送邮件、配置管理等。 Repository 实现, MQ 适配, DB Config

现代变体:六边形架构 (Hexagonal Architecture)

不要和上面的严格分成两种架构,要把他们融合在一起,他们都是 DDD

在实际开发中,为了让领域层 彻底独立,现在更多公司采用依赖倒置 的模型。(我会单独画一篇文章去理解依赖倒置

  • 核心理念: 领域层在中间,所有的外部系统(数据库、前端、第三方 API)都叫"适配器"。
  • 关键点: 领域层定义接口(Ports) ,基础设施层去实现(Adapters)
  • 优势: 你的业务逻辑不依赖于你是用 MySQL 还是 Oracle,也不在乎你是用 Spring 还是 Micronaut。

一个标准的工程目录结构 (Java 示例)

看一个目录结构 就懂了,也有可能是模块结构

Plaintext 复制代码
com.company.project
├── interfaces (或 user)
│   ├── controller       // 接口入库
│   └── dto              // 返回给前端的 DTO
├── application
│   └── service          // 应用服务,负责调用领域层
├── domain
│   ├── model            // 聚合、实体、值对象
│   ├── service          // 领域服务(跨实体的逻辑)
│   └── repository       // 仓储接口 (注意:这里只有 interface)
└── infrastructure
    ├── repository       // 数据库实现 (MyBatis/JPA 实现类)
    ├── config           // 配置类
    └── external         // 调用其他微服务的 Feign 客户端

我认为的 DDD 的核心要义

  • 不要在应用层写业务逻辑: 应用层应该很薄,它只是个"指挥官",具体的判断逻辑(比如:余额够不够)必须在领域层。

  • 不要在领域层引用框架类: 尽量让 domain 文件夹里只有纯粹的 Java 代码,不要出现 HttpServletRequest 这种东西。

  • 资源库 (Repository) 的定义: 接口定义在 domain,具体实现在 infrastructure

依赖倒置

结论: 查询数据库的动作 (执行 SQL、调用驱动)一定不在 Domain 层,但查询数据库的契约(接口) 必须留在 Domain 层。

具体应该怎么放,取决于你的查询目的。我们需要区分两种场景:


1. 为了"执行业务逻辑"而查询 (Repository)

如果你需要查询数据来判断业务规则(例如:下单前查一下用户余额、查一下库存),这种情况遵循 "依赖倒置原则"

  • Domain 层: 定义一个接口(Repository Interface)。它表达的是业务意图:"我需要一个能根据 ID 获取聚合根的能力"。
  • Infrastructure 层: 实现这个接口。这里才真正写 MyBatis、JPA 或 SQL 语句。
  • Application 层: 负责调用这个接口。

代码流转示意:

  1. Application 层 调用 domainRepository.findById(id)
  2. Domain 层 拿到返回的实体(Entity),并在实体上执行业务方法(如 order.validate())。
  3. Application 层 根据结果决定是否提交事务。

2. 为了"页面展示"而查询 (CQRS)

如果你的查询仅仅是为了在 UI 上列表显示,不涉及任何业务逻辑(即:不需要判断,直接展示),那么可以绕过 Domain 层。

  • 做法:Application 层(或者专门的 Query 层)直接调用 Infrastructure 层的查询服务,返回 DTO 即可。
  • 原因: 领域模型(Domain Model)通常很重且复杂。如果只是为了前端展示一个表格,强行通过 Domain 层包装成实体再转成 DTO,性能差且链路冗余。
查询类型 逻辑归属 实际查询位置 最终返回对象
业务逻辑查询 Domain 定义接口 Infrastructure 实现 Entity / Aggregate (实体)
页面展示查询 Application / Query 层 Infrastructure 直接实现 DTO (纯数据对象)

具体实现

在标准的 DDD 实践中,我们会利用 依赖倒置原则 (DIP) 。即 Domain 层不依赖具体的数据库技术,而是定义"我需要什么",由 Infrastructure 层去实现"怎么拿数据"。

以下是基于 Spring Boot 的标准代码结构示例:

1. 目录结构预览

在 IDEA 中,你的包结构看起来应该是这样的:

Plaintext 复制代码
src/main/java/com/example/demo
├── domain
│   ├── model
│   │   └── User.java           // 聚合根 (Entity)
│   └── repository
│       └── UserRepository.java // 接口定义 (只含业务契约)
├── infrastructure
│   └── persistence
│       ├── UserMapper.java     // MyBatis/JPA 的具体实现
│       └── UserRepositoryImpl.java // 仓储实现类
└── application
    └── UserService.java        // 应用服务

2. 代码实现

第一步:Domain 层 - 定义接口

这里不包含任何 Spring Data 或 MyBatis 的注解,它是纯粹的业务契约。

Java 复制代码
// domain.repository.UserRepository
public interface UserRepository {
    // 根据 ID 获取用户,返回的是领域模型(Entity)
    User findById(Long id);
    
    // 保存用户
    void save(User user);
}
第二步:Infrastructure 层 - 真正实现

这里处理技术细节(如 SQL、缓存等)。我们通常会注入底层的 Mapper 或 Spring Data JPA。

Java 复制代码
// infrastructure.persistence.UserRepositoryImpl
@Repository // 只有实现类才打上 Spring 的持久化注解
public class UserRepositoryImpl implements UserRepository {

    @Autowired
    private UserMapper userMapper; // 假设使用 MyBatis

    @Override
    public User findById(Long id) {
        // 1. 调用底层的数据库操作(返回的是数据库实体 PO)
        UserPO userPO = userMapper.selectById(id);
        
        // 2. 将数据对象 (PO) 转换为领域对象 (Entity)
        // 这是为了保证 Domain 层的纯净,不被数据库字段定义污染
        return UserConverter.toEntity(userPO);
    }

    @Override
    public void save(User user) {
        UserPO po = UserConverter.toPO(user);
        userMapper.insertOrUpdate(po);
    }
}
第三步:Application 层 - 编排调用

应用服务只负责"指挥",它并不知道数据是怎么存的。

Java 复制代码
// application.UserService
@Service
public class UserService {

    @Autowired
    private UserRepository userRepository; // 注入接口,解耦!

    @Transactional
    public void changeUserEmail(Long id, String newEmail) {
        // 1. 通过仓储加载聚合根
        User user = userRepository.findById(id);
        
        // 2. 执行领域逻辑
        user.updateEmail(newEmail);
        
        // 3. 保存回仓储
        userRepository.save(user);
    }
}

3. 为什么一定要这么写?(深度理解)

  1. 屏蔽技术细节 :如果哪天你要把数据存到 Redis 或者远程 API,你只需要在 infrastructure 层写一个新的实现类,domain 层和 application 层的一行代码都不用动。
  2. 保护业务逻辑User 实体是一个具有业务行为的对象(比如有 changePassword() 方法),而数据库对应的 UserPO 只是一个简单的字段容器。通过转换,防止数据库的字段设计(如 is_deleted)渗透到业务逻辑中。
  3. 方便单元测试 :在测试 UserService 时,你可以非常轻松地 Mock 掉 UserRepository 接口,而不需要启动真正的数据库。

核心差异点:PO vs Entity

  • PO (Persistent Object) : 对应数据库表结构,属性全公开(Getter/Setter)。UserPO.java
  • Entity (Domain Model) : 对应业务模型,属性通常私有,通过方法暴露业务动作。User.java
相关推荐
掘金者阿豪2 小时前
Kubernetes 中 "Deployment does not have minimum availability" 错误解析与解决方案
后端
Java编程爱好者2 小时前
SpringBoot+SPI机制,轻松实现可插拔组件
后端
无限大62 小时前
为什么"容器化"技术很重要?——从虚拟机到 Docker
后端·github
qq_12498707532 小时前
基于springboot的智能任务管理助手小程序设计与实现(源码+论文+部署+安装)
spring boot·后端·信息可视化·微信小程序·小程序·毕业设计·计算机毕业设计
狂炫冰美式2 小时前
Meta 收购 Manus:当巨头搭台时,你要做那个递钥匙的人
前端·人工智能·后端
狂奔小菜鸡2 小时前
Day36 | Java中的线程池技术
java·后端·java ee
嘻哈baby2 小时前
Go context详解:超时控制与请求链路追踪
后端