一、学习目标
- 理解 DDD 的核心概念:通用语言、实体、值对象、聚合根、聚合、领域服务、应用服务。
- 掌握 限界上下文(Bounded Context) 与分层架构在实际项目中的落地方式。
- 学会 CQRS(命令查询职责分离) 的基本模式:Command / Query / Handler 的分工。
- 能在 Spring 项目中实现一个简化的 DDD + CQRS 订单模块(含领域模型、应用服务、仓储接口、事件发布)。
- 为以后向 事件驱动架构(Event-Driven) 演进打下基础。
二、DDD 核心概念与简单建模
2.1 通用语言与限界上下文
-
通用语言(Ubiquitous Language)
- 与产品、业务、测试共同约定一套 领域词汇表,在需求文档、代码、接口文档中都保持一致。
- 例如:订单状态用
CREATED / PAID / SHIPPED / COMPLETED,而不是混用 "已下单/已支付/配送中/完成"。
-
限界上下文(Bounded Context)
- 将大型系统按业务划分成多个相对独立的子域,例如:
user-context:用户、权限order-context:订单、订单明细payment-context:支付、退款
- 不同上下文之间通过 API / 事件 协作,而不是直接共享数据库表。
- 将大型系统按业务划分成多个相对独立的子域,例如:
可以先用文字描述一个简单的订单业务上下文:
- 用户在商城下单(创建订单)
- 用户支付订单(订单状态从 CREATED -> PAID)
- 商家发货(状态从 PAID -> SHIPPED)
- 用户确认收货(状态从 SHIPPED -> COMPLETED)
2.2 实体(Entity)和值对象(Value Object)
-
概念:
- 实体 :有唯一标识(ID),生命周期内状态可以变化,例如:
Order、User。 - 值对象 :无独立身份,只关心值本身,通常是不可变的,例如:
Money、Address。
- 实体 :有唯一标识(ID),生命周期内状态可以变化,例如:
-
代码示例:值对象
Money和Address
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 聚合与聚合根
- 聚合(Aggregate):一组相关对象的集合,对外表现为一个整体。
- 聚合根(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;
}
}