Java学习第37天 - 领域驱动设计(DDD)与 CQRS 实战

一、学习目标

  • 理解 DDD 的核心概念:通用语言、实体、值对象、聚合根、聚合、领域服务、应用服务。
  • 掌握 限界上下文(Bounded Context) 与分层架构在实际项目中的落地方式。
  • 学会 CQRS(命令查询职责分离) 的基本模式:Command / Query / Handler 的分工。
  • 能在 Spring 项目中实现一个简化的 DDD + CQRS 订单模块(含领域模型、应用服务、仓储接口、事件发布)。
  • 为以后向 事件驱动架构(Event-Driven) 演进打下基础。

二、DDD 核心概念与简单建模

2.1 通用语言与限界上下文

  1. 通用语言(Ubiquitous Language)

    • 与产品、业务、测试共同约定一套 领域词汇表,在需求文档、代码、接口文档中都保持一致。
    • 例如:订单状态用 CREATED / PAID / SHIPPED / COMPLETED,而不是混用 "已下单/已支付/配送中/完成"。
  2. 限界上下文(Bounded Context)

    • 将大型系统按业务划分成多个相对独立的子域,例如:
      • user-context:用户、权限
      • order-context:订单、订单明细
      • payment-context:支付、退款
    • 不同上下文之间通过 API / 事件 协作,而不是直接共享数据库表。

可以先用文字描述一个简单的订单业务上下文:

  • 用户在商城下单(创建订单)
  • 用户支付订单(订单状态从 CREATED -> PAID)
  • 商家发货(状态从 PAID -> SHIPPED)
  • 用户确认收货(状态从 SHIPPED -> COMPLETED)

2.2 实体(Entity)和值对象(Value Object)

  1. 概念:

    • 实体 :有唯一标识(ID),生命周期内状态可以变化,例如:OrderUser
    • 值对象 :无独立身份,只关心值本身,通常是不可变的,例如:MoneyAddress
  2. 代码示例:值对象 MoneyAddress

java 复制代码
import java.math.BigDecimal;
import java.util.Objects;

// 值对象:金额
public class Money {

    private final BigDecimal amount;
    private final String currency; // 货币代码:CNY, USD...

    public Money(BigDecimal amount, String currency) {
        if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("金额不能为负数");
        }
        if (currency == null || currency.isBlank()) {
            throw new IllegalArgumentException("货币不能为空");
        }
        this.amount = amount;
        this.currency = currency;
    }

    public Money add(Money other) {
        checkSameCurrency(other);
        return new Money(this.amount.add(other.amount), this.currency);
    }

    public Money subtract(Money other) {
        checkSameCurrency(other);
        BigDecimal result = this.amount.subtract(other.amount);
        if (result.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("金额不能为负数");
        }
        return new Money(result, this.currency);
    }

    private void checkSameCurrency(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("货币类型不一致");
        }
    }

    public BigDecimal getAmount() {
        return amount;
    }

    public String getCurrency() {
        return currency;
    }

    // 值对象必须基于值比较相等性
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Money)) return false;
        Money money = (Money) o;
        return amount.compareTo(money.amount) == 0 &&
                currency.equals(money.currency);
    }

    @Override
    public int hashCode() {
        return Objects.hash(amount.stripTrailingZeros(), currency);
    }

    @Override
    public String toString() {
        return amount + " " + currency;
    }
}
java 复制代码
// 值对象:收货地址
public class Address {

    private final String province;
    private final String city;
    private final String detail;

    public Address(String province, String city, String detail) {
        if (province == null || province.isBlank()) {
            throw new IllegalArgumentException("省份不能为空");
        }
        if (city == null || city.isBlank()) {
            throw new IllegalArgumentException("城市不能为空");
        }
        if (detail == null || detail.isBlank()) {
            throw new IllegalArgumentException("详细地址不能为空");
        }
        this.province = province;
        this.city = city;
        this.detail = detail;
    }

    public String getProvince() {
        return province;
    }

    public String getCity() {
        return city;
    }

    public String getDetail() {
        return detail;
    }

    @Override
    public String toString() {
        return province + " " + city + " " + detail;
    }
}

2.3 聚合与聚合根

  1. 聚合(Aggregate):一组相关对象的集合,对外表现为一个整体。
  2. 聚合根(Aggregate Root):聚合中的"入口对象",外部只能通过聚合根访问/修改内部对象。

订单聚合示例:Order(聚合根) + OrderItem(订单行)。

java 复制代码
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Order {

    public enum Status {
        CREATED, PAID, SHIPPED, COMPLETED, CANCELED
    }

    private Long id;                       // 实体 ID
    private Long userId;                   // 下单用户
    private List<OrderItem> items = new ArrayList<>(); // 订单行
    private Money totalPrice;              // 总价(值对象)
    private Status status;                 // 订单状态
    private LocalDateTime createdAt;
    private LocalDateTime paidAt;
    private LocalDateTime completedAt;

    // 聚合根创建逻辑封装在构造方法/工厂方法中
    public Order(Long userId, List<OrderItem> items) {
        if (userId == null) {
            throw new IllegalArgumentException("用户ID不能为空");
        }
        if (items == null || items.isEmpty()) {
            throw new IllegalArgumentException("订单行不能为空");
        }
        this.userId = userId;
        this.items.addAll(items);
        this.totalPrice = calculateTotalPrice(items);
        this.status = Status.CREATED;
        this.createdAt = LocalDateTime.now();
    }

    private Money calculateTotalPrice(List<OrderItem> items) {
        Money total = new Money(BigDecimal.ZERO, "CNY");
        for (OrderItem item : items) {
            total = total.add(item.getSubtotal());
        }
        return total;
    }

    // 业务行为:支付
    public void pay() {
        if (status != Status.CREATED) {
            throw new IllegalStateException("只有已创建的订单才能支付");
        }
        this.status = Status.PAID;
        this.paidAt = LocalDateTime.now();
    }

    // 业务行为:确认收货
    public void complete() {
        if (status != Status.SHIPPED) {
            throw new IllegalStateException("只有已发货的订单才能完成");
        }
        this.status = Status.COMPLETED;
        this.completedAt = LocalDateTime.now();
    }

    // 防止外部直接修改内部集合
    public List<OrderItem> getItems() {
        return Collections.unmodifiableList(items);
    }

    public Money getTotalPrice() {
        return totalPrice;
    }

    public Status getStatus() {
        return status;
    }

    public Long getUserId() {
        return userId;
    }

    // 省略 getter/setter ...
}
java 复制代码
import java.math.BigDecimal;

// 订单行:属于订单聚合内部实体
public class OrderItem {

    private Long productId;
    private String productName;
    private int quantity;
    private Money unitPrice;

    public OrderItem(Long productId, String productName, int quantity, Money unitPrice) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("数量必须大于 0");
        }
        this.productId = productId;
        this.productName = productName;
        this.quantity = quantity;
        this.unitPrice = unitPrice;
    }

    public Money getSubtotal() {
        return unitPrice.multiply(new BigDecimal(quantity));
    }

    // 省略 getter ...
}

三、DDD 分层架构与模块划分

3.1 典型分层结构

order-context 为例,一个常见的目录结构:

  • domain 领域层:纯业务逻辑,不依赖具体技术框架
    • model:实体、值对象、聚合根
    • repository:仓储接口(仅接口)
    • service:领域服务(复杂业务规则)
    • event:领域事件
  • application 应用层:编排用例流程
    • service:应用服务,调用领域对象、仓储、外部服务
    • command/query:命令和查询对象
  • infrastructure 基础设施层:技术细节实现
    • repository:JPA/MyBatis 实现仓储接口
    • client:HTTP/RPC 调用其他服务
    • config:Spring 配置
  • interfaces 接口层(或 adapter):Controller、DTO、VO

示例包结构(Java):

text 复制代码
com.example.order
 ├── domain
 │    ├── model
 │    │     ├── Order.java
 │    │     └── OrderItem.java
 │    ├── repository
 │    │     └── OrderRepository.java  // 接口
 │    ├── service
 │    │     └── OrderDomainService.java
 │    └── event
 │          └── OrderPaidEvent.java
 ├── application
 │    ├── command
 │    │     ├── CreateOrderCommand.java
 │    │     └── PayOrderCommand.java
 │    ├── query
 │    │     └── GetOrderDetailQuery.java
 │    └── service
 │          └── OrderApplicationService.java
 ├── infrastructure
 │    ├── repository
 │    │     └── OrderRepositoryJpaImpl.java
 │    └── config
 │          └── OrderConfig.java
 └── interfaces
      └── rest
           └── OrderController.java

3.2 仓储接口与实现分离

仓储接口(领域层):

java 复制代码
package com.example.order.domain.repository;

import com.example.order.domain.model.Order;

import java.util.Optional;

public interface OrderRepository {

    Optional<Order> findById(Long id);

    void save(Order order);
}

基础设施层实现(举例用 JPA):

java 复制代码
package com.example.order.infrastructure.repository;

import com.example.order.domain.model.Order;
import com.example.order.domain.repository.OrderRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public class OrderRepositoryJpaImpl implements OrderRepository {

    private final OrderJpaDao jpaDao; // Spring Data JPA 接口

    public OrderRepositoryJpaImpl(OrderJpaDao jpaDao) {
        this.jpaDao = jpaDao;
    }

    @Override
    public Optional<Order> findById(Long id) {
        return jpaDao.findById(id)
                .map(this::convertToDomain);
    }

    @Override
    public void save(Order order) {
        OrderEntity entity = convertToEntity(order);
        jpaDao.save(entity);
    }

    private Order convertToDomain(OrderEntity entity) {
        // 将 JPA 实体转换为领域模型
        // 这里省略字段映射细节
        return null;
    }

    private OrderEntity convertToEntity(Order order) {
        // 将领域模型转换为 JPA 实体
        return null;
    }
}

四、CQRS 基本模式:命令与查询分离

4.1 Command 与 Query 的职责

  • Command(命令):改变系统状态的操作(创建订单、支付订单、取消订单)。
  • Query(查询):只读取数据不改变状态(获取订单详情、订单列表)。

命令对象示例:

java 复制代码
// 创建订单命令
public class CreateOrderCommand {

    private Long userId;
    private List<CreateOrderItem> items;
    private String remark;

    // 内部类:创建订单行
    public static class CreateOrderItem {
        private Long productId;
        private String productName;
        private int quantity;
        private BigDecimal unitPrice;

        // getter/setter 省略
    }

    // getter/setter 省略
}

查询对象示例:

java 复制代码
// 查询订单详情
public class GetOrderDetailQuery {

    private Long orderId;

    public GetOrderDetailQuery(Long orderId) {
        this.orderId = orderId;
    }

    public Long getOrderId() {
        return orderId;
    }
}

4.2 CommandHandler / QueryHandler

可以为每个 Command 定义一个处理器(Handler),方便后续做扩展(如事件总线、拦截器、审计日志)。

java 复制代码
// 命令处理接口
public interface CommandHandler<C, R> {
    R handle(C command);
}

// 查询处理接口
public interface QueryHandler<Q, R> {
    R handle(Q query);
}

创建订单命令处理器:

java 复制代码
import com.example.order.domain.model.Order;
import com.example.order.domain.repository.OrderRepository;

import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;

public class CreateOrderCommandHandler implements CommandHandler<CreateOrderCommand, Long> {

    private final OrderRepository orderRepository;

    public CreateOrderCommandHandler(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Override
    public Long handle(CreateOrderCommand command) {
        List<OrderItem> items = command.getItems().stream()
                .map(i -> new OrderItem(
                        i.getProductId(),
                        i.getProductName(),
                        i.getQuantity(),
                        new Money(i.getUnitPrice(), "CNY")
                ))
                .collect(Collectors.toList());

        Order order = new Order(command.getUserId(), items);
        // 可以在这里调用领域服务、发布领域事件等

        orderRepository.save(order);
        return order.getId(); // 假设保存后 ID 已写回
    }
}

订单详情查询处理器:

java 复制代码
public class GetOrderDetailQueryHandler implements QueryHandler<GetOrderDetailQuery, OrderDetailDTO> {

    private final OrderReadModelRepository readModelRepository;

    public GetOrderDetailQueryHandler(OrderReadModelRepository readModelRepository) {
        this.readModelRepository = readModelRepository;
    }

    @Override
    public OrderDetailDTO handle(GetOrderDetailQuery query) {
        // CQRS:查询可以直接走读库/ES/缓存等
        return readModelRepository.findDetailById(query.getOrderId())
                .orElseThrow(() -> new IllegalArgumentException("订单不存在"));
    }
}

五、应用服务:编排用例与事务边界

5.1 应用服务与领域服务的区别

  • 应用服务(Application Service)
    • 面向用例:一个方法对应一个业务用例,如"创建订单并发支付请求"。
    • 负责事务边界、调用顺序、与外部系统交互。
  • 领域服务(Domain Service)
    • 纯业务规则,操作领域对象(实体/值对象/聚合根)。
    • 尽量不直接处理基础设施细节。

5.2 简化的订单应用服务(使用命令处理器)

java 复制代码
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderApplicationService {

    private final CreateOrderCommandHandler createOrderHandler;
    private final GetOrderDetailQueryHandler getOrderDetailHandler;

    public OrderApplicationService(CreateOrderCommandHandler createOrderHandler,
                                   GetOrderDetailQueryHandler getOrderDetailHandler) {
        this.createOrderHandler = createOrderHandler;
        this.getOrderDetailHandler = getOrderDetailHandler;
    }

    @Transactional
    public Long createOrder(CreateOrderCommand command) {
        // 可以在这里做权限校验、幂等等
        return createOrderHandler.handle(command);
    }

    @Transactional(readOnly = true)
    public OrderDetailDTO getOrderDetail(GetOrderDetailQuery query) {
        return getOrderDetailHandler.handle(query);
    }
}

六、领域事件与简单事件发布

6.1 领域事件对象

java 复制代码
import java.time.LocalDateTime;

// 订单已支付领域事件
public class OrderPaidEvent {
    private final Long orderId;
    private final Long userId;
    private final LocalDateTime paidAt;

    public OrderPaidEvent(Long orderId, Long userId, LocalDateTime paidAt) {
        this.orderId = orderId;
        this.userId = userId;
        this.paidAt = paidAt;
    }

    public Long getOrderId() {
        return orderId;
    }

    public Long getUserId() {
        return userId;
    }

    public LocalDateTime getPaidAt() {
        return paidAt;
    }
}

6.2 聚合中产生事件

Order 聚合中支付成功时记录事件:

java 复制代码
public class Order {

    // ... 之前的字段 ...

    private List<Object> domainEvents = new ArrayList<>();

    public void pay() {
        if (status != Status.CREATED) {
            throw new IllegalStateException("只有已创建的订单才能支付");
        }
        this.status = Status.PAID;
        this.paidAt = LocalDateTime.now();

        // 产生领域事件
        domainEvents.add(new OrderPaidEvent(this.id, this.userId, this.paidAt));
    }

    public List<Object> getDomainEvents() {
        return Collections.unmodifiableList(domainEvents);
    }

    public void clearDomainEvents() {
        domainEvents.clear();
    }
}

6.3 事件发布(简单 Spring 应用事件示例)

java 复制代码
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

@Component
public class DomainEventPublisher {

    private final ApplicationEventPublisher applicationEventPublisher;

    public DomainEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }

    public void publish(Object event) {
        applicationEventPublisher.publishEvent(event);
    }
}

在仓储保存订单后统一发布事件:

java 复制代码
@Repository
public class OrderRepositoryJpaImpl implements OrderRepository {

    private final OrderJpaDao jpaDao;
    private final DomainEventPublisher eventPublisher;

    public OrderRepositoryJpaImpl(OrderJpaDao jpaDao, DomainEventPublisher eventPublisher) {
        this.jpaDao = jpaDao;
        this.eventPublisher = eventPublisher;
    }

    @Override
    public void save(Order order) {
        OrderEntity entity = convertToEntity(order);
        jpaDao.save(entity);

        // 发布领域事件
        order.getDomainEvents().forEach(eventPublisher::publish);
        order.clearDomainEvents();
    }
}

监听事件:

java 复制代码
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class OrderEventHandler {

    @EventListener
    public void onOrderPaid(OrderPaidEvent event) {
        // 例如:增加积分、发消息、推送通知等
        System.out.println("订单已支付,ID = " + event.getOrderId());
    }
}

七、接口层:基于 DDD + CQRS 的 Controller

java 复制代码
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    private final OrderApplicationService orderApplicationService;

    public OrderController(OrderApplicationService orderApplicationService) {
        this.orderApplicationService = orderApplicationService;
    }

    @PostMapping
    public Long createOrder(@RequestBody CreateOrderRequest request) {
        CreateOrderCommand command = toCommand(request);
        return orderApplicationService.createOrder(command);
    }

    @GetMapping("/{id}")
    public OrderDetailDTO getOrderDetail(@PathVariable Long id) {
        GetOrderDetailQuery query = new GetOrderDetailQuery(id);
        return orderApplicationService.getOrderDetail(query);
    }

    private CreateOrderCommand toCommand(CreateOrderRequest request) {
        // 将 REST 请求 DTO 转换为 Command
        // 这里可以做参数校验、默认值处理等
        return null;
    }
}
相关推荐
米糕闯编程4 小时前
xshell使用CentOS10 root用户登录,权限问题
java·linux
sxhcwgcy4 小时前
Python中的简单爬虫
java
xiaoliuliu123454 小时前
Android Studio 2025 安装教程:详细步骤+自定义安装路径+SDK配置(附桌面快捷方式创建)
java·前端·数据库
老前端的功夫4 小时前
【Java从入门到入土】21:List三剑客:ArrayList、LinkedList、Vector的爱恨情仇
java·javascript·网络·python·list
SAP小崔说事儿4 小时前
SAP B1 批量应用用户界面配置模板
java·前端·ui·sap·b1·无锡sap
电商API&Tina5 小时前
唯品会数据采集API接口||电商API数据采集
java·javascript·数据库·python·sql·json
人机与认知实验室5 小时前
Maven与以色列福音系统有何区别?
java·maven
wuqingshun3141595 小时前
spring如何解决循环依赖问题的?
java