第16章:领域驱动设计(DDD)¶
领域驱动设计(Domain-Driven Design)是一种以业务领域为核心的软件设计方法论,通过将复杂业务逻辑显式建模来驾驭软件复杂性。
目录¶
- 1. DDD概述与核心思想
- 2. 战略设计
- 3. 战术设计
- 4. 事件驱动架构与CQRS
- 5. DDD分层架构
- 6. Spring Boot DDD实战
- 7. 面试题精选
- 8. 检查清单
- 9. 推荐资源
1. DDD概述与核心思想¶
1.1 什么是DDD¶
DDD由Eric Evans在2003年《Domain-Driven Design: Tackling Complexity in the Heart of Software》一书中提出,核心主张:
- 业务复杂性才是软件的根本挑战——技术复杂性是次要的
- 领域模型是团队沟通和代码实现的统一语言
- 持续精炼模型:模型不是一次设计完成的,需要随业务理解加深而演化
1.2 核心原则¶
| 原则 | 说明 |
|---|---|
| 统一语言(Ubiquitous Language) | 开发者与领域专家使用同一套术语 |
| 模型驱动设计 | 代码直接反映领域模型 |
| 限界上下文 | 明确模型适用的边界 |
| 持续集成 | 模型与代码保持同步 |
| 知识消化 | 从领域专家处不断提炼知识 |
1.3 DDD vs 传统三层架构¶
传统三层架构:
┌─────────────────┐
│ 表示层 (UI) │
├─────────────────┤
│ 业务逻辑层 │ ← 贫血模型,Service承载所有逻辑
├─────────────────┤
│ 数据访问层 │
└─────────────────┘
DDD分层架构:
┌─────────────────┐
│ 用户接口层 │
├─────────────────┤
│ 应用层 │ ← 编排用例,不含业务规则
├─────────────────┤
│ 领域层 │ ← 充血模型,核心业务逻辑
├─────────────────┤
│ 基础设施层 │ ← 技术实现细节
└─────────────────┘
2. 战略设计¶
2.1 限界上下文(Bounded Context)¶
限界上下文是DDD战略设计的核心概念,定义了模型的适用范围和语义边界。
电商系统限界上下文划分:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 商品上下文 │ │ 订单上下文 │ │ 支付上下文 │
│ │ │ │ │ │
│ Product │ │ Order │ │ Payment │
│ Category │ │ OrderItem│ │ Account │
│ SKU │ │ Address │ │ Transaction│
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└─────── 上下文映射 ────────┘
同一术语在不同上下文中有不同含义:
- "Product" 在商品上下文中包含详细描述、规格参数
- "Product" 在订单上下文中只是一个ID + 名称 + 价格的快照
- "Account" 在支付上下文中是资金账户
- "Account" 在用户上下文中是登录账户
2.2 上下文映射(Context Mapping)¶
上下文之间的关系模式:
| 映射模式 | 说明 | 典型场景 |
|---|---|---|
| 合作关系(Partnership) | 两个团队协同演进 | 核心业务的紧密子域 |
| 共享内核(Shared Kernel) | 共享一小部分模型 | 共用的领域对象 |
| 客户-供应商(Customer-Supplier) | 上游供应,下游消费 | 订单 → 支付 |
| 遵奉者(Conformist) | 下游完全遵从上游模型 | 对接第三方API |
| 防腐层(Anti-Corruption Layer) | 下游翻译上游模型 | 对接遗留系统 |
| 开放主机服务(Open Host Service) | 提供标准化协议 | 对外暴露REST API |
| 发布语言(Published Language) | 使用公认的交换模型 | Protobuf/JSON Schema |
2.3 子域划分¶
┌─────────────────────────────────────────┐
│ 电商业务域 │
├────────────┬────────────┬───────────────┤
│ 核心子域 │ 支撑子域 │ 通用子域 │
│ │ │ │
│ • 交易流程 │ • 库存管理 │ • 用户认证 │
│ • 营销策略 │ • 物流跟踪 │ • 消息通知 │
│ • 推荐算法 │ • 客服工单 │ • 文件存储 │
│ │ │ │
│ 战略价值最高 │ 必要但非核心 │ 可外采/通用方案 │
└────────────┴────────────┴───────────────┘
3. 战术设计¶
3.1 实体(Entity)¶
实体具有唯一标识,通过ID区分,而非属性值。
public class Order {
private OrderId id; // 唯一标识
private CustomerId customerId;
private List<OrderItem> items;
private OrderStatus status;
private Money totalAmount;
// 实体的業務行为
public void addItem(Product product, int quantity) {
validateCanModify();
OrderItem item = new OrderItem(product, quantity);
items.add(item);
recalculateTotal();
}
public void confirm() {
if (items.isEmpty()) {
throw new DomainException("订单不能为空");
}
this.status = OrderStatus.CONFIRMED;
registerEvent(new OrderConfirmedEvent(this.id));
}
private void validateCanModify() {
if (status != OrderStatus.DRAFT) {
throw new DomainException("只有草稿状态的订单可以修改");
}
}
}
3.2 值对象(Value Object)¶
值对象没有唯一标识,通过属性值相等来判断是否相同,且不可变。
// 值对象(Value Object):无唯一标识,通过属性值相等性判断,且不可变
// 使用Java record实现:自动生成equals/hashCode/toString
public record Money(BigDecimal amount, Currency currency) {
// 紧凑构造器(Compact Constructor):在创建时校验业务规则
public Money {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("金额不能为负"); // 保护不变量
}
Objects.requireNonNull(currency, "货币不能为空");
}
// 值对象的行为:返回新对象而非修改自身(不可变性)
public Money add(Money other) {
assertSameCurrency(other); // 跨币种计算保护
return new Money(amount.add(other.amount), currency);
}
public Money multiply(int quantity) {
return new Money(amount.multiply(BigDecimal.valueOf(quantity)), currency);
}
// 私有校验:确保两个金额的币种一致
private void assertSameCurrency(Money other) {
if (!currency.equals(other.currency)) {
throw new DomainException("货币类型不一致");
}
}
}
// 地址值对象:通过属性组合描述一个完整的概念
public record Address(String province, String city, String district, String detail) {
// 构造时校验必填字段
public Address {
Objects.requireNonNull(province);
Objects.requireNonNull(city);
}
// 值对象可以包含有意义的业务方法
public String fullAddress() {
return province + city + district + detail;
}
}
3.3 聚合与聚合根(Aggregate & Aggregate Root)¶
聚合是一组相关对象的集合,聚合根是外部访问聚合的唯一入口。
聚合设计原则:
- 聚合内保持一致性——事务边界在聚合内部
- 聚合间通过ID引用——不直接持有其他聚合的引用
- 聚合尽量小——只包含必须保持一致性的对象
- 通过聚合根修改内部状态——外部不能绕过聚合根
// === 聚合根(Aggregate Root) ===
// Order是聚合根,外部只能通过Order操作内部的OrderItem
public class Order {
private OrderId id;
private List<OrderItem> items; // OrderItem属于Order聚合内部,不能被外部直接访问
// 聚合根提供操作内部对象的方法,保护业务不变量
public void addItem(ProductId productId, String name, Money price, int qty) {
OrderItem item = new OrderItem(productId, name, price, qty);
this.items.add(item);
}
// 通过聚合根删除内部实体
public void removeItem(ProductId productId) {
items.removeIf(i -> i.getProductId().equals(productId));
}
}
// OrderItem不是聚合根,生命周期依赖于Order
public class OrderItem {
private ProductId productId; // 通过ID引用其他聚合(而非直接持有Product对象)
private String productName; // 快照数据:订单创建时的商品名称
private Money unitPrice; // 快照数据:订单创建时的单价
private int quantity;
// 计算小计金额(利用值对象Money的行为)
Money subtotal() {
return unitPrice.multiply(quantity);
}
}
3.4 领域事件(Domain Event)¶
领域事件表示领域中已经发生的有意义的事情,用于解耦聚合间的交互。
// === 领域事件定义 ===
// 用过去式命名(OrderConfirmed),表示“已经发生的事实”
public record OrderConfirmedEvent(
OrderId orderId,
CustomerId customerId,
Money totalAmount,
LocalDateTime occurredAt // 事件发生时间
) implements DomainEvent {
// 便捷构造器:自动填充事件发生时间
public OrderConfirmedEvent(OrderId orderId, CustomerId customerId, Money totalAmount) {
this(orderId, customerId, totalAmount, LocalDateTime.now());
}
}
// === 在聚合根中发布事件 ===
// 继承AbstractAggregateRoot获得事件发布能力(Spring Data提供)
public class Order extends AbstractAggregateRoot<Order> {
public void confirm() {
// 业务校验...
this.status = OrderStatus.CONFIRMED;
// 注册领域事件,会在仓储save()时自动发布
registerEvent(new OrderConfirmedEvent(id, customerId, totalAmount));
}
}
// === 事件处理器 ===
// 在另一个限界上下文中监听并处理事件,实现聚合间解耦
@Component
public class OrderEventHandler {
// @EventListener:同步事件监听(与发布者在同一事务内)
@EventListener
public void onOrderConfirmed(OrderConfirmedEvent event) {
// 触发库存扣减(跨聚合操作)
inventoryService.deductStock(event.orderId());
}
// @TransactionalEventListener:在事务提交后才执行,避免事务回滚导致重复通知
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendNotification(OrderConfirmedEvent event) {
// 发送订单确认通知(异步操作,不影响主流程)
notificationService.notify(event.customerId(), "订单已确认");
}
}
3.5 领域服务(Domain Service)¶
当业务逻辑不自然属于任何一个实体或值对象时,使用领域服务。
// 领域服务:处理跨实体/跨聚合的业务逻辑,不自然属于任何单个实体
@DomainService
public class PricingService {
// 订单价格计算:涉及订单和客户两个聚合,故放在领域服务中
public Money calculateOrderPrice(Order order, Customer customer) {
Money basePrice = order.calculateSubtotal(); // 订单基础价
// 第一步:根据会员等级应用折扣
Money discounted = applyMemberDiscount(basePrice, customer.getLevel());
// 第二步:应用满减优惠活动
Money afterPromo = applyPromotions(discounted, order.getItems());
return afterPromo;
}
// 会员折扣策略:使用模式匹配根据等级确定折扣率
private Money applyMemberDiscount(Money price, MemberLevel level) {
BigDecimal rate = switch (level) {
case GOLD -> new BigDecimal("0.9"); // 金牌会员九折
case SILVER -> new BigDecimal("0.95"); // 银牌会员九五折
default -> BigDecimal.ONE; // 普通用户不打折
};
return price.multiply(rate);
}
}
3.6 仓储(Repository)¶
仓储为聚合提供持久化抽象,屏蔽底层存储细节。
// === 仓储接口(定义在领域层) ===
// 领域层只定义接口,不依赖任何持久化技术
public interface OrderRepository {
Optional<Order> findById(OrderId id); // 按ID查找聚合
void save(Order order); // 保存聚合(新增或更新)
void delete(Order order); // 删除聚合
List<Order> findByCustomerId(CustomerId customerId); // 按客户ID查询
}
// === 仓储实现(基础设施层) ===
// 使用JPA实现领域层定义的接口,体现“依赖倒置原则”
@Repository
public class JpaOrderRepository implements OrderRepository {
@PersistenceContext
private EntityManager em; // JPA实体管理器
@Override
public Optional<Order> findById(OrderId id) {
// 查询持久化实体,然后通过Mapper转换为领域对象
return Optional.ofNullable(em.find(OrderEntity.class, id.value()))
.map(OrderMapper::toDomain); // 数据库实体 → 领域对象的转换
}
@Override
public void save(Order order) {
OrderEntity entity = OrderMapper.toEntity(order); // 领域对象 → 数据库实体
em.merge(entity); // merge支持新增和更新
}
}
4. 事件驱动架构与CQRS¶
4.1 事件驱动架构(EDA)¶
┌──────────┐ Event ┌──────────────┐ Event ┌──────────┐
│ 订单服务 │ ─────────→ │ 消息中间件 │ ─────────→ │ 库存服务 │
│ │ │ (Kafka/RMQ) │ │ │
└──────────┘ │ │ └──────────┘
│ │ Event ┌──────────┐
│ │ ─────────→ │ 通知服务 │
└──────────────┘ └──────────┘
EDA的优势:
- 松耦合:生产者不需要知道消费者的存在
- 可扩展:新增消费者不影响现有系统
- 弹性:消费者可以独立伸缩
- 最终一致性:适合分布式系统
4.2 CQRS模式(命令查询职责分离)¶
┌──────────────────────────────────────┐
│ CQRS 架构 │
│ │
Command │ ┌──────────┐ ┌───────────────┐ │
──────────→ │ │ 命令处理器 │───→│ 写模型(领域) │ │
│ └──────────┘ └───────┬───────┘ │
│ │ 事件 │
│ ▼ │
│ ┌──────────────┐ │
│ │ 事件存储 │ │
│ └──────┬───────┘ │
│ │ 投影 │
│ ▼ │
Query │ ┌──────────┐ ┌───────────────┐ │
──────────→ │ │ 查询处理器 │───→│ 读模型(视图) │ │
│ └──────────┘ └───────────────┘ │
└──────────────────────────────────────┘
CQRS实现:
// === CQRS命令端:负责写操作 ===
// 命令对象:封装写操作的意图和数据
public record CreateOrderCommand(
CustomerId customerId,
List<OrderItemRequest> items,
Address shippingAddress
) {}
// 命令处理器:编排用例,调用领域对象执行业务逻辑
@Service
public class OrderCommandHandler {
private final OrderRepository orderRepository;
private final PricingService pricingService;
@Transactional // 命令处理在事务内执行,保证原子性
public OrderId handle(CreateOrderCommand cmd) {
// 1. 创建聚合根
Order order = Order.create(cmd.customerId(), cmd.shippingAddress());
// 2. 添加订单项(通过聚合根方法保护业务规则)
cmd.items().forEach(item ->
order.addItem(item.productId(), item.name(), item.price(), item.qty())
);
// 3. 持久化聚合
orderRepository.save(order);
return order.getId();
}
}
// === CQRS查询端:负责读操作,可以使用专门优化的读模型 ===
// 查询对象:封装查询条件和分页参数
public record OrderSummaryQuery(CustomerId customerId, int page, int size) {}
// 查询处理器:直接从读优化的视图中查询,不经过领域模型
@Service
public class OrderQueryHandler {
private final OrderReadRepository readRepository; // 读专用仓储,可能是ElasticSearch/Redis等
public Page<OrderSummaryDTO> handle(OrderSummaryQuery query) {
return readRepository.findSummariesByCustomer(
query.customerId(), query.page(), query.size()
);
}
}
4.3 事件溯源(Event Sourcing)¶
// === 事件溯源(Event Sourcing)的聚合根 ===
// 不直接存储当前状态,而是存储所有事件,通过回放事件重建状态
public class Order {
private OrderId id;
private OrderStatus status;
private List<DomainEvent> uncommittedEvents = new ArrayList<>(); // 未提交的新事件
// 从历史事件序列重建聚合状态(每次加载时调用)
public static Order reconstitute(List<DomainEvent> events) {
Order order = new Order();
events.forEach(order::apply); // 依次回放每个事件
return order;
}
// 业务操作:只产生事件,不直接修改状态
public void confirm() {
raise(new OrderConfirmedEvent(id, LocalDateTime.now()));
}
// 产生事件:先apply更新状态,再加入未提交列表
private void raise(DomainEvent event) {
apply(event); // 立即更新内存状态
uncommittedEvents.add(event); // 记录待持久化的事件
}
// 事件处理:根据事件类型更新聚合状态(模式匹配)
private void apply(DomainEvent event) {
switch (event) {
case OrderCreatedEvent e -> {
this.id = e.orderId();
this.status = OrderStatus.DRAFT; // 创建时设为草稿状态
}
case OrderConfirmedEvent e -> {
this.status = OrderStatus.CONFIRMED; // 确认时转为已确认
}
default -> throw new IllegalArgumentException("Unknown event");
}
}
}
4.4 事件溯源的幂等性(Idempotency)¶
事件溯源系统中,幂等性是保证数据一致性的关键,需要在三个层面解决:
1. 命令端幂等:防止重复命令产生重复事件
@Service
public class IdempotentCommandHandler {
private final EventStore eventStore;
private final IdempotencyStore idempotencyStore; // Redis/DB
@Transactional
public OrderId handle(CreateOrderCommand cmd, String idempotencyKey) {
// 1. 检查命令是否已处理过
if (idempotencyStore.exists(idempotencyKey)) {
return idempotencyStore.getResult(idempotencyKey, OrderId.class);
}
// 2. 执行命令
Order order = Order.create(cmd.customerId(), cmd.shippingAddress());
eventStore.append(order.getId(), order.getUncommittedEvents());
// 3. 记录幂等键(与事件写入在同一事务中)
idempotencyStore.save(idempotencyKey, order.getId(), Duration.ofHours(24));
return order.getId();
}
}
2. 事件存储幂等:防止重复事件写入
public interface EventStore {
/**
* 追加事件(乐观并发控制)
* @param expectedVersion 期望的当前版本号,防止并发写入
* @throws OptimisticConcurrencyException 版本冲突时抛出
*/
void append(AggregateId id, List<DomainEvent> events, long expectedVersion);
}
// 实现:每个事件携带递增版本号
@Entity
@Table(uniqueConstraints = @UniqueConstraint(
columnNames = {"aggregate_id", "version"} // 联合唯一约束保证幂等
))
public class EventRecord {
@Id @GeneratedValue
private Long id;
private String aggregateId;
private long version; // 聚合内递增版本号
private String eventType;
private String payload;
private Instant occurredAt;
}
3. 事件消费端幂等:投影(Projection)的幂等处理
@Component
public class OrderProjection {
private final OrderReadRepository readRepo;
private final ProjectionOffsetStore offsetStore;
/**
* 处理事件时记录offset,确保重放安全
* 即使消息被重复投递(如Kafka rebalance),也不会产生副作用
*/
@Transactional
public void handle(DomainEvent event, long offset) {
// 1. 检查是否已处理过此偏移量
if (offsetStore.isProcessed("order-projection", offset)) {
return; // 幂等跳过
}
// 2. 更新读模型
switch (event) {
case OrderCreatedEvent e -> readRepo.insert(toSummary(e));
case OrderConfirmedEvent e -> readRepo.updateStatus(
e.orderId(), OrderStatus.CONFIRMED
);
}
// 3. 记录已处理偏移量
offsetStore.markProcessed("order-projection", offset);
}
}
要点总结:Event Sourcing 系统中实现幂等的核心手段: - 命令端:幂等键(Idempotency Key)去重 - 存储端:乐观锁(版本号)+ 唯一约束 - 消费端:偏移量追踪(Offset Tracking)或事件ID去重
5. DDD分层架构¶
5.1 四层架构详解¶
┌───────────────────────────────────────────────────┐
│ 用户接口层 (Interfaces) │
│ Controller / DTO / Assembler / 前端适配 │
├───────────────────────────────────────────────────┤
│ 应用层 (Application) │
│ ApplicationService / CommandHandler / DTO │
│ 编排用例、事务管理、权限检查 │
├───────────────────────────────────────────────────┤
│ 领域层 (Domain) │
│ Entity / ValueObject / Aggregate / DomainService │
│ Repository接口 / DomainEvent / Factory │
│ ★ 核心业务逻辑,不依赖任何外部技术 ★ │
├───────────────────────────────────────────────────┤
│ 基础设施层 (Infrastructure) │
│ Repository实现 / ORM映射 / 消息中间件适配 │
│ 外部API网关 / 缓存实现 │
└───────────────────────────────────────────────────┘
5.2 依赖方向¶
5.3 各层职责对比¶
| 层次 | 职责 | 依赖 | 典型类 |
|---|---|---|---|
| 用户接口层 | HTTP适配、参数校验、DTO转换 | 应用层 | Controller, DTO |
| 应用层 | 用例编排、事务、权限 | 领域层 | ApplicationService |
| 领域层 | 核心业务逻辑 | 无外部依赖 | Entity, VO, DomainService |
| 基础设施层 | 技术实现细节 | 领域层接口 | JpaRepository, KafkaPublisher |
6. Spring Boot DDD实战¶
6.1 项目结构¶
order-service/
├── src/main/java/com/example/order/
│ ├── interfaces/ # 用户接口层
│ │ ├── rest/
│ │ │ ├── OrderController.java
│ │ │ └── dto/
│ │ │ ├── CreateOrderRequest.java
│ │ │ └── OrderResponse.java
│ │ └── assembler/
│ │ └── OrderAssembler.java
│ │
│ ├── application/ # 应用层
│ │ ├── OrderApplicationService.java
│ │ ├── command/
│ │ │ └── CreateOrderCommand.java
│ │ └── query/
│ │ └── OrderQueryService.java
│ │
│ ├── domain/ # 领域层
│ │ ├── model/
│ │ │ ├── Order.java # 聚合根
│ │ │ ├── OrderItem.java # 实体
│ │ │ ├── OrderId.java # 值对象
│ │ │ ├── Money.java # 值对象
│ │ │ └── OrderStatus.java # 枚举
│ │ ├── event/
│ │ │ └── OrderConfirmedEvent.java
│ │ ├── service/
│ │ │ └── PricingService.java
│ │ └── repository/
│ │ └── OrderRepository.java # 接口
│ │
│ └── infrastructure/ # 基础设施层
│ ├── persistence/
│ │ ├── JpaOrderRepository.java
│ │ ├── entity/
│ │ │ └── OrderEntity.java
│ │ └── mapper/
│ │ └── OrderMapper.java
│ ├── messaging/
│ │ └── KafkaEventPublisher.java
│ └── config/
│ └── DomainEventConfig.java
6.2 完整代码示例¶
// === Spring Boot DDD完整代码示例 ===
// === 用户接口层:处理HTTP请求,参数校验,DTO转换 ===
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderApplicationService orderAppService;
private final OrderAssembler assembler; // DTO与命令对象的转换器
@PostMapping
public ResponseEntity<OrderResponse> createOrder(@RequestBody @Valid CreateOrderRequest req) {
CreateOrderCommand cmd = assembler.toCommand(req); // 请求DTO → 命令对象
OrderId orderId = orderAppService.createOrder(cmd);
return ResponseEntity.status(HttpStatus.CREATED)
.body(new OrderResponse(orderId.value()));
}
@PostMapping("/{orderId}/confirm")
public ResponseEntity<Void> confirmOrder(@PathVariable String orderId) {
orderAppService.confirmOrder(new OrderId(orderId));
return ResponseEntity.ok().build();
}
}
// === 应用层:编排用例流程,不包含业务规则 ===
@Service
@Transactional // 应用层管理事务边界
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final PricingService pricingService; // 领域服务
private final CustomerRepository customerRepository;
// 创建订单用例:应用层负责编排,领域层负责业务规则
public OrderId createOrder(CreateOrderCommand cmd) {
// 1. 获取客户聚合
Customer customer = customerRepository.findById(cmd.customerId())
.orElseThrow(() -> new EntityNotFoundException("客户不存在"));
// 2. 创建订单聚合根(工厂方法)
Order order = Order.create(cmd.customerId(), cmd.shippingAddress());
// 3. 通过聚合根方法添加订单项
cmd.items().forEach(item ->
order.addItem(item.productId(), item.name(), item.price(), item.quantity())
);
// 4. 调用领域服务计算最终价格
Money finalPrice = pricingService.calculateOrderPrice(order, customer);
order.applyFinalPrice(finalPrice);
// 5. 持久化聚合(仓储会自动发布领域事件)
orderRepository.save(order);
return order.getId();
}
// 确认订单用例
public void confirmOrder(OrderId orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new EntityNotFoundException("订单不存在"));
order.confirm(); // 调用聚合根的业务方法
orderRepository.save(order);
}
}
// === 领域层 - 聚合根:包含核心业务逻辑,不依赖任何外部框架 ===
public class Order extends AbstractAggregateRoot<Order> {
private OrderId id;
private CustomerId customerId;
private Address shippingAddress;
private List<OrderItem> items = new ArrayList<>();
private OrderStatus status;
private Money totalAmount;
private LocalDateTime createdAt;
// 静态工厂方法:封装聚合创建逻辑,确保初始状态合法
public static Order create(CustomerId customerId, Address address) {
Order order = new Order();
order.id = OrderId.generate(); // 生成唯一标识
order.customerId = customerId;
order.shippingAddress = address;
order.status = OrderStatus.DRAFT; // 初始状态为草稿
order.createdAt = LocalDateTime.now();
return order;
}
// 添加商品:聚合根保护业务不变量 + 自动重算总额
public void addItem(ProductId productId, String name, Money price, int qty) {
if (status != OrderStatus.DRAFT) {
throw new DomainException("只有草稿订单可以添加商品");
}
items.add(new OrderItem(productId, name, price, qty));
recalculateTotal(); // 保持总额与订单项一致
}
// 确认订单:状态转换 + 发布领域事件
public void confirm() {
if (items.isEmpty()) {
throw new DomainException("空订单不能确认");
}
if (status != OrderStatus.DRAFT) {
throw new DomainException("当前状态不允许确认");
}
this.status = OrderStatus.CONFIRMED;
registerEvent(new OrderConfirmedEvent(id, customerId, totalAmount)); // 发布事件
}
// 私有方法:汇总所有订单项小计金额
private void recalculateTotal() {
this.totalAmount = items.stream()
.map(OrderItem::subtotal) // 获取每项小计
.reduce(Money.ZERO, Money::add); // 累加求和
}
}
// === 基础设施层 - 仓储实现:将领域对象与数据库实体解耦 ===
@Repository
public class JpaOrderRepository implements OrderRepository {
private final SpringDataOrderRepository springRepo; // Spring Data JPA自动实现
private final OrderMapper mapper; // 领域对象↔数据库实体双向转换
@Override
public Optional<Order> findById(OrderId id) {
return springRepo.findById(id.value()).map(mapper::toDomain);
}
@Override
public void save(Order order) {
OrderEntity entity = mapper.toEntity(order);
springRepo.save(entity);
}
}
6.3 防腐层示例¶
// === 防腐层(Anti-Corruption Layer) ===
// 用于对接第三方系统,将外部模型翻译为领域模型,防止外部概念污染领域层
// 领域层定义的支付网关接口
public interface PaymentGateway { // interface定义类型契约
PaymentResult pay(OrderId orderId, Money amount);
}
// 基础设施层的适配器实现:将支付宝 SDK 转换为领域接口
@Component
public class AlipayGatewayAdapter implements PaymentGateway { // extends继承;implements实现接口
private final AlipayClient alipayClient; // 第三方支付宝SDK
@Override // @Override重写父类方法
public PaymentResult pay(OrderId orderId, Money amount) {
// 1. 领域模型 → 第三方格式(出站转换)
AlipayTradePayRequest request = new AlipayTradePayRequest();
request.setOutTradeNo(orderId.value());
request.setTotalAmount(amount.amount().toString());
// 2. 调用第三方API
AlipayTradePayResponse response = alipayClient.execute(request);
// 3. 第三方结果 → 领域模型(入站转换)
return response.isSuccess()
? PaymentResult.success(response.getTradeNo())
: PaymentResult.failure(response.getSubMsg());
}
}
7. 面试题精选¶
Q1: 实体和值对象的区别?¶
实体(Entity): - 有唯一标识(ID) - 通过ID判断是否相同 - 可变,有生命周期 - 示例:用户、订单
值对象(Value Object): - 没有唯一标识 - 通过属性值判断是否相等 - 不可变 - 示例:金额、地址、日期范围
Q2: 聚合的设计原则有哪些?¶
- 保护业务不变量:聚合边界内保持强一致性
- 尽量小:只包含必须同时变化的对象
- 通过ID引用其他聚合:不持有其他聚合的直接引用
- 一个事务只修改一个聚合:聚合间通过事件通信
- 通过聚合根访问:外部不能绕过聚合根修改内部状态
Q3: CQRS适用什么场景?不适用什么场景?¶
适用: - 读写负载差异大(读多写少) - 读模型和写模型差异大 - 需要独立扩展读写 - 事件溯源系统
不适用: - 简单CRUD应用 - 读写模型几乎一致 - 实时性要求极高(CQRS引入延迟) - 团队对DDD理解不足
Q4: 领域事件和集成事件的区别?¶
| 维度 | 领域事件 | 集成事件 |
|---|---|---|
| 范围 | 限界上下文内部 | 跨限界上下文 |
| 传输 | 进程内(Spring Event) | 消息中间件(Kafka) |
| 一致性 | 强一致 | 最终一致 |
| 格式 | 领域对象 | DTO/序列化对象 |
Q5: 如何让贫血模型演进到充血模型?¶
- 识别业务规则:找出Service中的if-else逻辑
- 归还行为:将属于实体的行为移到实体内部
- 引入值对象:把基本类型包装为有行为的值对象
- 保护不变量:在构造/修改时校验业务规则
- 逐步重构:不要一次性改造,按聚合逐步演进
Q6: DDD中如何处理跨聚合的事务?¶
- 方案一:Saga模式 —— 通过一系列本地事务+补偿机制实现
- 方案二:领域事件 —— 聚合A提交后发布事件,聚合B监听并处理
- 方案三:Process Manager —— 通过流程管理器协调多个聚合
- 反模式:在一个事务中修改多个聚合(破坏聚合边界)
8. 检查清单¶
战略设计检查¶
- 是否识别并划分了核心域、支撑域、通用域?
- 限界上下文边界是否清晰?
- 上下文映射关系是否明确?
- 统一语言词汇表是否建立?
- 每个上下文是否有独立的团队负责?
战术设计检查¶
- 实体是否有唯一标识?
- 值对象是否不可变?
- 聚合边界是否足够小?
- 聚合根是否保护了业务不变量?
- 是否通过ID引用其他聚合(而非直接引用)?
- 领域事件是否用过去式命名(OrderCreated, PaymentCompleted)?
- Repository接口是否定义在领域层?
架构检查¶
- 领域层是否零外部依赖?
- 应用层是否只编排用例而不含业务逻辑?
- 基础设施层是否实现了领域层定义的接口?
- 是否使用防腐层对接外部系统?
- DTO和领域对象是否严格分离?
9. 推荐资源¶
书籍¶
- 《领域驱动设计》—— Eric Evans(蓝皮书)
- 《实现领域驱动设计》—— Vaughn Vernon(红皮书)
- 《领域驱动设计精粹》—— Vaughn Vernon(入门推荐)
- 《解构领域驱动设计》—— 张逸
在线资源¶
- DDD Community —— Eric Evans官方
- Martin Fowler - DDD
- Microsoft DDD Guidance
开源项目¶
- ddd-starter-modelling-process —— DDD建模流程
- event-storming —— 事件风暴方法论