一、为什么需要领域驱动设计?
在当今复杂的软件系统开发中,我们常常面临这样的困境:业务逻辑分散在各处,系统难以理解,变更成本高昂。传统的分层架构虽然提供了清晰的代码组织方式,却无法有效解决业务复杂性问题。这正是领域驱动设计(Domain-Driven Design,简称DDD)应运而生的背景。2003年,Eric Evans出版了《领域驱动设计》一书,提出了一套全新的软件设计方法论。DDD不是框架,也不是技术栈,而是一种思维方式,一套指导我们如何分析复杂业务领域并将业务知识融入软件设计的实践原则。
二、DDD的基本概念
2.1 什么是领域?
领域(Domain)是指软件系统要解决的业务问题空间。
例如:
- 电商系统中的"订单处理"、"库存管理"
- 银行系统中的"账户管理"、"交易处理"
2.2 通用语言
通用语言是DDD中最基础也最重要的概念之一。它是指开发团队和业务专家共同创建的、用于精确描述领域模型的共享语言。
2.3 限界上下文(Bounded Context)
限界上下文
是DDD中最核心的战略设计模式,它定义了特定模型应用的边界,在这个边界内,术语、概念和规则具有明确一致的含义。
在复杂系统中,同一个术语在不同部门可能有不同含义。
例如电商系统中:
- 营销上下文中的"产品":关注名称、描述、促销信息
- 库存上下文中的"产品":关注SKU、库存量、仓库位置
- 物流上下文中的"产品":关注重量、尺寸、运输要求
如果不明确划分边界,会导致:
- 模型混乱,术语含义模糊
- 代码耦合度高,修改困难
- 团队沟通效率低下
限界上下文的实现形式:
实现形式 | 描述 | 适用场景 |
---|---|---|
独立微服务 | 完全独立的部署单元 | 高规模、需独立扩展的上下文 |
模块/包 | 同一应用内的逻辑隔离 | 中小系统,上下文耦合较高 |
独立应用 | 单独部署的传统应用 | 遗留系统集成 |
移动客户端 | 移动端特定实现 | 需要离线能力的场景 |
2.4 上下文映射图(Context Mapping)
上下文映射图是描述限界上下文之间关系的战略设计工具,它展示了:
- 不同上下文如何相互关联
- 集成点的位置和性质
- 团队间的协作模式
2.4.1 合作关系: 两个上下文相互依赖,必须协同演化示例,通过适配器来表现这种合作关系:
// 订单上下文直接调用支付上下文的适配器
public class OrderService {
private final PaymentAdapter paymentAdapter;
public void completeOrder(Order order) {
// ...
PaymentResult result = paymentAdapter.processPayment(
order.getId(),
order.getTotalAmount(),
order.getPaymentMethod());
// ...
}
}
2.4.2 共享内核: 两个或多个上下文共享一部分通用模型
// 共享内核模块 - shared-kernel
public class Address {
private String street;
private String city;
private String postalCode;
// 严格维护的通用方法...
}
// 订单上下文依赖shared-kernel
public class Order {
private Address shippingAddress;
// ...
}
// 客户上下文也依赖shared-kernel
public class Customer {
private Address primaryAddress;
// ...
}
2.4.3 客户-供应商: 上游(Supplier)上下文服务于下游(Customer)上下文
// 上游上下文提供的客户端库
public class ProductCatalogClient {
public ProductInfo getProductDetails(ProductId id) {
// 调用上游上下文的API
}
}
// 下游上下文的使用
public class OrderLine {
private final ProductCatalogClient catalogClient;
public Money calculatePrice() {
ProductInfo info = catalogClient.getProductDetails(this.productId);
return info.getBasePrice().multiply(quantity);
}
}
2.4.4 遵奉者: 下游上下文遵奉上游模型,不做转换
// 直接使用上游的模型类
public class OrderReport {
public void generateReport(List<UpstreamOrder> orders) {
// 直接使用上游的Order类
for (UpstreamOrder order : orders) {
// ...
}
}
}
2.4.5 防腐层:下游上下文通过转换层隔离上游模型的影响
// 防腐层实现
public class LegacyOrderAdapter {
public DomainOrder translate(LegacyOrder legacyOrder) {
return new DomainOrder(
convertId(legacyOrder.getOrderNo()),
convertItems(legacyOrder.getLineItems()),
convertStatus(legacyOrder.getStatusCode())
);
}
private OrderId convertId(String legacyId) {
// 复杂的ID转换逻辑
}
// 其他转换方法...
}
// 领域代码使用干净的领域模型
public class OrderProcessor {
private final LegacyOrderAdapter adapter;
public void process(LegacyOrder legacyOrder) {
DomainOrder order = adapter.translate(legacyOrder);
// 使用干净的领域模型继续处理
}
}
2.4.6 开放主机服务:上下文定义明确的协议供其他上下文调用
三、DDD中的战术设计
战术设计是领域驱动设计(DDD)中构建精细领域模型的关键部分,它提供了一系列构建块和模式来创建富有表现力的领域模型。核心概念包括实体、值对象、聚合根、领域服务、领域事件等。
3.1 实体(Entity)
实体是领域模型中具有唯一标识和连续性的对象。即使其属性发生变化,它仍然是同一个实体。实体的核心特征包括:
- 唯一标识:通过ID而非属性来区分
- 可变性:属性可以随时间改变
- 连续性:在整个生命周期中保持身份不变
示例:酒店预定系统中的客户实体
public class Customer {
// 唯一标识
private final CustomerId id;
private String name;
private Email email;
private CustomerStatus status;
private List<Address> addresses;
// 构造函数
public Customer(CustomerId id, String name, Email email) {
this.id = Objects.requireNonNull(id);
this.name = validateName(name);
this.email = Objects.requireNonNull(email);
this.status = CustomerStatus.REGULAR;
this.addresses = new ArrayList<>();
}
// 业务行为
public void changeName(String newName) {
this.name = validateName(newName);
}
public void addAddress(Address address) {
if (addresses.size() >= 5) {
throw new BusinessRuleViolationException("Customer cannot have more than 5 addresses");
}
this.addresses.add(address);
}
public enum CustomerStatus {
REGULAR, PREMIUM, VIP
}
}
3.2 值对象(Value Object)
值对象是没有概念上身份的对象,它们完全通过属性值来定义。核心特征包括:
- 无标识:通过属性值而非ID区分
- 不可变性:创建后不能被修改
- 可替换性:可以整个替换而非修改
- 自包含验证:创建时即确保有效性
示例:货币值对象
public final class Money implements Comparable<Money> {
private final BigDecimal amount;
private final Currency currency;
// 私有构造函数
private Money(BigDecimal amount, Currency currency) {
this.amount = amount.setScale(currency.getDefaultFractionDigits(), RoundingMode.HALF_EVEN);
this.currency = Objects.requireNonNull(currency);
}
// 工厂方法
public static Money of(BigDecimal amount, Currency currency) {
Objects.requireNonNull(amount);
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Money amount cannot be negative");
}
return new Money(amount, currency);
}
public static Money zero(Currency currency) {
return new Money(BigDecimal.ZERO, currency);
}
// 业务操作
public Money add(Money other) {
checkSameCurrency(other);
return new Money(this.amount.add(other.amount), this.currency);
}
// 比较操作
@Override
public int compareTo(Money other) {
checkSameCurrency(other);
return this.amount.compareTo(other.amount);
}
// 相等性比较
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money money = (Money) o;
return amount.compareTo(money.amount) == 0 &&
currency.equals(money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount, currency);
}
}
3.3 聚合根(Aggregate Root)
聚合是一组相关对象的集合,作为数据修改的单元。每个聚合都有一个根实体(聚合根),外部对象只能引用聚合根。核心特征包括:
- 一致性边界:聚合内保证强一致性
- 根实体:外部只能通过根访问内部对象
- 事务边界:通常一个事务只修改一个聚合
- 全局标识:聚合根具有全局唯一ID
示例:订单聚合
public class Order {
// 聚合根标识
private final OrderId id;
private CustomerId customerId;
private OrderStatus status;
private List<OrderItem> items;
private Money totalAmount;
private Address shippingAddress;
// 构造函数
private Order(OrderId id, CustomerId customerId, Address shippingAddress) {
this.id = Objects.requireNonNull(id);
this.customerId = Objects.requireNonNull(customerId);
this.status = OrderStatus.CREATED;
this.items = new ArrayList<>();
this.totalAmount = Money.zero(Currency.getInstance("USD"));
this.shippingAddress = Objects.requireNonNull(shippingAddress);
}
// 工厂方法
public static Order create(OrderId id, CustomerId customerId, Address shippingAddress) {
return new Order(id, customerId, shippingAddress);
}
// 业务行为
public void addItem(ProductId productId, String productName,
Money unitPrice, int quantity) {
if (status != OrderStatus.CREATED) {
throw new OrderOperationException("Cannot modify order in current state");
}
if (quantity <= 0) {
throw new IllegalArgumentException("Quantity must be positive");
}
// 检查是否已存在相同产品
items.stream()
.filter(item -> item.getProductId().equals(productId))
.findFirst()
.ifPresentOrElse(
existing -> existing.increaseQuantity(quantity),
() -> items.add(new OrderItem(productId, productName, unitPrice, quantity))
);
// 重新计算总金额
recalculateTotal();
}
public void submit() {
if (items.isEmpty()) {
throw new OrderOperationException("Cannot submit empty order");
}
this.status = OrderStatus.SUBMITTED;
DomainEventPublisher.instance()
.publish(new OrderSubmitted(id, customerId, totalAmount));
}
public void cancel() {
if (status == OrderStatus.SHIPPED || status == OrderStatus.DELIVERED) {
throw new OrderOperationException("Cannot cancel order in current state");
}
this.status = OrderStatus.CANCELLED;
}
// 私有方法
private void recalculateTotal() {
this.totalAmount = items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.zero(totalAmount.getCurrency()), Money::add);
}
// 内部实体
private static class OrderItem {
private final ProductId productId;
private final String productName;
private final Money unitPrice;
private int quantity;
// 构造函数和方法...
public Money getSubtotal() {
return unitPrice.multiply(new BigDecimal(quantity));
}
}
}
3.4 领域服务(Domain Service)
领域服务用于处理不适合放在实体或值对象中的领域逻辑,特别是:
- 涉及多个聚合的协调
- 与外部系统交互
- 复杂的业务规则实现
示例:订单支付服务
public interface PaymentService {
PaymentResult processPayment(OrderPaymentCommand command);
}
public class OrderPaymentProcessor implements PaymentService {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
private final DomainEventPublisher eventPublisher;
@Transactional
public PaymentResult processPayment(OrderPaymentCommand command) {
Order order = orderRepository.findById(command.orderId())
.orElseThrow(() -> new OrderNotFoundException(command.orderId()));
// 验证订单状态
if (order.getStatus() != OrderStatus.SUBMITTED) {
throw new OrderPaymentException("Order is not in payable state");
}
// 调用外部支付网关
PaymentGatewayResponse response = paymentGateway.charge(
command.paymentMethodToken(),
order.getTotalAmount(),
"Order payment: " + order.getId());
// 根据结果更新订单状态
if (response.isSuccess()) {
order.markAsPaid(response.transactionId());
eventPublisher.publish(new OrderPaid(
order.getId(),
order.getCustomerId(),
order.getTotalAmount(),
response.transactionId()));
return PaymentResult.success(response.transactionId());
} else {
order.markAsPaymentFailed(response.errorMessage());
return PaymentResult.failed(response.errorMessage());
}
}
}
// 命令对象
public record OrderPaymentCommand(
OrderId orderId,
String paymentMethodToken,
String customerIpAddress
) {
public OrderPaymentCommand {
Objects.requireNonNull(orderId);
if (paymentMethodToken == null || paymentMethodToken.isBlank()) {
throw new IllegalArgumentException("Payment method token required");
}
}
}
// 支付结果
public record PaymentResult(
boolean success,
String transactionId,
String errorMessage
) {
public static PaymentResult success(String transactionId) {
return new PaymentResult(true, transactionId, null);
}
public static PaymentResult failed(String errorMessage) {
return new PaymentResult(false, null, errorMessage);
}
}
3.5 领域事件(Domain Event)
领域事件表示领域中发生的重要事情,用于:
- 通知其他聚合或限界上下文
- 保持最终一致性
- 实现事件溯源
- 触发后续业务流程
示例:订单相关事件
// 基础事件接口
public interface DomainEvent {
Instant occurredOn();
}
// 订单创建事件
public class OrderCreated(
OrderId orderId,
CustomerId customerId,
Money totalAmount,
Instant occurredOn
) implements DomainEvent {
public OrderCreated(OrderId orderId, CustomerId customerId, Money totalAmount) {
this(orderId, customerId, totalAmount, Instant.now());
}
}
// 订单支付事件
public class OrderPaid(
OrderId orderId,
CustomerId customerId,
Money amount,
String transactionId,
Instant occurredOn
) implements DomainEvent {
public OrderPaid(OrderId orderId, CustomerId customerId, Money amount, String transactionId) {
this(orderId, customerId, amount, transactionId, Instant.now());
}
}
// 事件处理器示例
public class OrderStatusUpdater {
@EventListener
public void handleOrderPaid(OrderPaid event) {
// 更新订单状态为已支付
// 可能触发发货流程等
}
}
// 跨上下文集成示例
public class InventoryUpdater {
@EventListener
public void handleOrderCreated(OrderCreated event) {
// 减少库存
// 可能位于不同的限界上下文
}
}
四、高级主题(CQRS和事件溯源)
4.1 CQRS的概念
命令查询职责分离(Command Query Responsibility Segregation,CQRS)模式源自Bertrand Meyer提出的**CQS(Command-Query Separation)**原则,该原则认为对象的方法应该严格分为两类:
- 命令(Command):改变系统状态但不返回数据
- 查询(Query):返回数据但不改变系统状态
CQRS模式将系统分为两个独立的部分:
命令端(Command Side)
- 处理所有状态变更操作(创建、更新、删除)
- 通过命令对象接收请求
- 产生领域事件记录状态变化
- 通常采用领域模型实现复杂业务逻辑
// 命令对象示例
public class PlaceOrderCommand {
private UUID customerId;
private List<OrderItem> items;
private Address shippingAddress;
// 构造函数、验证逻辑、getter方法...
}
// 命令处理器
public class OrderCommandHandler {
private final OrderRepository repository;
public void handle(PlaceOrderCommand command) {
Order order = new Order(
command.getCustomerId(),
command.getItems(),
command.getShippingAddress()
);
repository.save(order);
// 发布OrderPlaced事件...
}
}
查询端(Query Side)
- 专为数据展示优化
- 使用简化的数据模型(DTO、视图模型)
- 可直接查询非规范化的数据存储
- 支持复杂查询而不影响命令端模型
// 查询专用DTO
public class OrderSummaryDto {
private String orderId;
private String customerName;
private BigDecimal totalAmount;
private String status;
// 简单getter/setter...
}
// 查询服务
public class OrderQueryService {
private final OrderSummaryRepository repository;
public List<OrderSummaryDto> getCustomerOrders(UUID customerId) {
return repository.findByCustomerId(customerId);
}
}
4.2 事件溯源(Event Sourcing)
事件溯源是一种持久化模式,它不存储聚合的当前状态,而是存储导致状态变化的一系列事件。通过重放这些事件,可以重建聚合的任何历史状态
关键内容:4.2.1 事件(Event)表示领域发生的事实,通常用过去式命名(如OrderPlaced)
public class OrderPlacedEvent implements DomainEvent {
private UUID orderId;
private UUID customerId;
private List<OrderItem> items;
private Instant occurredAt;
// getters, constructors...
}
4.2.2 事件存储(Event Store)专门存储事件的数据库,支持:
- 按聚合ID检索事件流
- 追加新事件(不允许修改)
- 可能支持订阅新事件
4.2.3 聚合重建
通过重放事件恢复聚合状态
public class Order extends AggregateRoot {
private OrderStatus status;
private List<OrderItem> items;
// 应用事件改变状态
protected void apply(OrderPlacedEvent event) {
this.status = OrderStatus.PLACED;
this.items = event.getItems();
}
// 从事件流重建聚合
public static Order recreateFrom(List<DomainEvent> history) {
Order order = new Order();
history.forEach(order::apply);
return order;
}
}
4.2.3 它与传统持久化的对比
方面 | 传统持久化 | 事件溯源 |
---|---|---|
存储内容 | 当前状态 | 状态变更事件 |
历史追溯 | 有限(需额外设计) | 完整历史 |
数据丢失 | 可能覆盖旧数据 | 零数据丢失 |
审计能力 | 需要额外日志 | 内置强大审计 |
4.2.4 快照对于 生命周期长 或 变更频繁 的聚合,完全重放所有事件可能性能低下。解决方案是定期创建 快照 :
public class AccountSnapshot {
private UUID accountId;
private BigDecimal balance;
private int version; // 对应最后一个事件的版本
// ...
}