面向中级开发者的领域驱动设计(Domain-Driven Design)实战指南。
从战略设计到代码落地,构建清晰、可维护的复杂业务系统。
DDD(领域驱动设计)不是一个复杂的框架,而是一种思考方式。核心思想可以用三句话概括:
| 传统写法(事务脚本) | DDD 写法(领域模型) |
|---|---|
// Service 里充斥业务逻辑 public void withdraw(Long accountId, BigDecimal amount) { Account account = dao.findById(accountId); if (account.getBalance().compareTo(amount) < 0) { throw new Exception("余额不足"); } account.setBalance(account.getBalance().subtract(amount)); dao.update(account); } |
// 业务逻辑在实体内部 public class Account { public void withdraw(Money amount) { if (balance.lessThan(amount)) { throw new InsufficientBalanceException(); } this.balance = balance.subtract(amount); addEvent(new WithdrawalMadeEvent()); } } |
在深入代码之前,必须理解 DDD 的战略设计。核心在于将复杂的业务领域划分为多个独立的限界上下文(Bounded Context)。 每个上下文都有明确的边界,内部拥有统一的通用语言(Ubiquitous Language)。
上下文映射(Context Map)则定义了不同上下文之间的集成关系(如防腐层 ACL、开放主机服务 OHS 等)。
并非所有项目都需要 DDD。以下清单帮助你判断:
| 判断维度 | 适合 DDD ✅ | 不适合 DDD ❌ |
|---|---|---|
| 业务复杂度 | 复杂业务规则、多状态流转、跨多个业务实体的决策逻辑 | 简单的 CRUD、数据报表、工具类系统 |
| 项目生命周期 | 长期维护(3年+)、需求持续变化 | 一次性项目、原型验证、短期活动页面 |
| 团队规模 | 5人以上、有业务专家配合 | 1-2人小团队、无业务专家 |
| 系统规模 | 中大型系统、多子域、微服务架构 | 小型系统、单体应用、后台管理系统 |
项目是否有复杂业务逻辑?
├─ 否 → 使用事务脚本或 CRUD 框架(如 Spring Data JPA)
└─ 是 → 业务规则是否频繁变化?
├─ 否 → 可以使用传统三层架构
└─ 是 → 团队是否有 DDD 经验?
├─ 否 → 先学习,小模块试点
└─ 是 → 全面采用 DDD
DDD 推荐的分层架构旨在将领域逻辑与技术实现分离。核心原则是依赖倒置(DIP):高层模块不应依赖低层模块,二者都应依赖其抽象。
图 1:DDD 四层架构及其依赖关系(注意基础设施层对领域层的依赖倒置)
我们将通过“创建订单”这一业务场景,展示每一层的具体实现。
职责:负责向用户展示信息和解释用户指令。它不包含业务逻辑,仅进行简单的数据校验和格式转换。
@RestController @RequestMapping("/orders") public class OrderController { private final OrderAppService orderAppService; // DTO (Data Transfer Object) 用于层间数据传输,避免暴露领域模型 @PostMapping public Result<String> createOrder(@RequestBody CreateOrderRequest request) { // 调用应用层服务 String orderId = orderAppService.createOrder( request.getCustomerId(), request.getItems() ); return Result.success(orderId); } }
职责:协调领域对象完成业务任务。它不包含业务规则,只负责编排流程(如事务控制、权限校验、发送事件)。
@Service public class OrderAppService { private final OrderRepository orderRepository; private final ProductService productService; // 外部服务 @Transactional public String createOrder(String customerId, List<OrderItemDTO> items) { // 1. 准备数据 (可能涉及其他上下文调用) Address address = customerService.getDefaultAddress(customerId); // 2. 调用领域层创建聚合根 Order order = Order.create(customerId, address); // 3. 执行业务逻辑 for (OrderItemDTO item : items) { order.addItem(item.getProductId(), item.getQuantity(), item.getPrice()); } // 4. 持久化 (通过 Repository 接口) orderRepository.save(order); return order.getId().getValue(); } }
职责:包含所有业务逻辑和领域知识。是业务软件的核心。
// 聚合根
public class Order extends AggregateRoot {
private OrderId id;
private OrderStatus status;
private List<OrderItem> items; // 实体列表
private Address shippingAddress; // 值对象
// 构造方法私有,通过工厂方法创建
private Order(String customerId, Address address) { ... }
// 业务行为:添加订单项
public void addItem(String productId, int quantity, BigDecimal price) {
if (status != OrderStatus.DRAFT) {
throw new DomainException("只能在草稿状态下添加商品");
}
this.items.add(new OrderItem(productId, quantity, price));
// 领域事件
addDomainEvent(new OrderItemAddedEvent(this.id, productId));
}
}
// 仓储接口 (定义在领域层)
public interface OrderRepository {
void save(Order order);
Order findById(OrderId id);
}
职责:为其他层提供技术能力(数据库持久化、消息队列、Redis、第三方 API)。
关键点:通过实现领域层的接口,完成依赖倒置。
@Repository public class OrderRepositoryImpl implements OrderRepository { private final JpaOrderDAO dao; // 具体 ORM 实现 @Override public void save(Order order) { // 将领域模型转换为数据模型 (Data Model / PO) OrderPO po = OrderConverter.toPO(order); dao.save(po); } }
业务需求:用户下单,需检查库存、计算价格(含优惠券)、扣库存、创建订单、发送通知
下图展示了“创建订单”请求在各层之间的流转过程。注意控制流与依赖关系的区别。
假设我们接手一个遗留系统,需要将贫血模型重构为充血模型
DDD 分层架构并非一成不变。以下是两种常见的演进架构:
| 架构模式 | 核心思想 | 适用场景 |
|---|---|---|
| 传统三层架构 | 数据驱动,自顶向下依赖 (UI -> Logic -> Data) | 简单的 CRUD 业务,无复杂逻辑 |
| DDD 分层架构 | 领域驱动,依赖倒置 (Infra 依赖 Domain) | 中大型复杂业务系统 |
| 六边形架构 (Ports & Adapters) | 内外分离,核心逻辑通过端口与外部交互 | 需要高度解耦,支持多种输入输出适配器 |
| 整洁架构 (Clean Arch) | 同心圆结构,依赖指向圆心 (Entities) | 追求极致的可测试性和框架无关性 |
定义:领域事件是领域模型中发生的、业务专家关心的重要状态变更。它是实现最终一致性和系统解耦的关键机制。
// 领域事件定义 public class OrderCreatedEvent extends DomainEvent { private final OrderId orderId; private final String customerId; private final BigDecimal totalAmount; private final Instant occurredOn; } // 在聚合根中发布事件 public class Order extends AggregateRoot { public static Order create(String customerId, Address address) { Order order = new Order(customerId, address); order.addDomainEvent(new OrderCreatedEvent(order.id, customerId)); return order; } } // 事件处理器 @EventHandler public class OrderEventHandler { public void handle(OrderCreatedEvent event) { // 发送邮件、更新库存、记录日志等 emailService.sendOrderConfirmation(event.getCustomerId()); } }
核心原则:
仓储 vs DAO:
| 维度 | Repository (仓储) | DAO (数据访问对象) |
|---|---|---|
| 层次 | 领域层概念,面向聚合 | 持久化层概念,面向数据表 |
| 操作对象 | 聚合根 (Aggregate Root) | 数据模型 (PO/DO) |
| 方法命名 | 业务语言 (findByOrderNumber) | 技术语言 (selectById) |
| 返回值 | 领域对象 | 数据对象 |
当与外部系统或遗留系统集成时,防腐层充当翻译器,保护领域模型不受外部模型污染。
// 防腐层实现 @Component public class LegacySystemACL { private final LegacyApiClient legacyClient; // 将外部模型转换为领域模型 public Product getProduct(String productId) { LegacyProductDTO dto = legacyClient.fetchProduct(productId); return Product.builder() .id(new ProductId(dto.getSkuCode())) .name(dto.getItemName()) .price(new Money(dto.getPriceInCents() / 100)) .build(); } }
// ❌ Entity 只有数据,无行为 public class Order { private String id; private String status; // 只有 getter/setter... } // 业务逻辑全在 Service 层 public class OrderService { public void cancel(Order order) { if (order.getStatus().equals("SHIPPED")) { throw new Exception(); } order.setStatus("CANCELLED"); } }
// ✅ Entity 包含业务逻辑 public class Order { private OrderId id; private OrderStatus status; // 业务行为封装在实体内部 public void cancel() { if (status == OrderStatus.SHIPPED) { throw new DomainException("已发货订单不能取消"); } this.status = OrderStatus.CANCELLED; addDomainEvent(new OrderCancelledEvent(this.id)); } }
值对象应该是不可变的,所有字段都是 final,通过构造函数或工厂方法创建。
public final class Money { private final BigDecimal amount; private final Currency currency; public Money(BigDecimal amount, Currency currency) { if (amount.compareTo(BigDecimal.ZERO) < 0) { throw new IllegalArgumentException("金额不能为负"); } this.amount = amount; this.currency = currency; } // 不提供 setter,通过新对象返回操作结果 public Money add(Money other) { if (!this.currency.equals(other.currency)) { throw new IllegalArgumentException("货币类型不匹配"); } return new Money(this.amount.add(other.amount), this.currency); } // 重写 equals 和 hashCode,基于值相等 @Override public boolean equals(Object o) { ... } }
何时使用领域服务:
// 领域服务示例:转账操作涉及两个账户聚合 @Service public class MoneyTransferDomainService { public void transfer(Account from, Account to, Money amount) { from.withdraw(amount); to.deposit(amount); // 发布领域事件 eventPublisher.publish(new TransferCompletedEvent(from.id, to.id, amount)); } }
当实体或聚合的创建逻辑复杂时,使用工厂模式封装创建细节。
public class OrderFactory { public Order createOrder(OrderCreationRequest request) { // 复杂的验证逻辑 validateCustomer(request.getCustomerId()); // 从多个来源组装数据 Address address = addressService.getAddress(request.getAddressId()); OrderId orderId = idGenerator.nextId(); // 创建订单 Order order = new Order(orderId, request.getCustomerId(), address); // 添加订单项 request.getItems().forEach(item -> order.addItem(item.getProductId(), item.getQuantity()) ); return order; } }
| 场景 | 命令模型(写) | 查询模型(读) |
|---|---|---|
| 目的 | 修改数据,保证业务规则 | 读取数据,性能优先 |
| 模型 | 领域模型(聚合) | DTO / View Model |
| 数据源 | 主库(强一致性) | 从库 / 缓存 / ES |