领域驱动设计(Domain-Driven Design)是应对复杂业务系统设计的工程方法论,通过统一语言、领域模型和限界上下文将业务复杂性控制在可管理范围内。DDD不是技术框架,而是设计理念,其核心价值在于让软件模型与业务领域保持一致。在微服务架构中,DDD的限界上下文为服务拆分提供了业务驱动的划分依据,而非纯技术维度的拆分。事件溯源模式作为DDD的高级实践,将状态变更以事件序列持久化,为系统审计、状态回放和CQRS读写分离提供基础设施支撑。
统一语言与领域模型:从业务需求到代码模型的映射
统一语言(Ubiquitous Language)是DDD的第一原则:业务专家和开发团队使用同一套术语描述领域概念,这些术语直接映射为代码中的类名、方法名和变量名。统一语言消除了业务需求到代码实现之间的翻译损耗。
// 以电商订单系统为例
// 统一语言词汇表:
// - 订单(Order): 用户提交的购买请求
// - 订单项(OrderItem): 订单中单个商品的购买记录
// - 下单(PlaceOrder): 用户提交订单的动作
// - 支付(Pay): 订单金额的结算动作
// - 发货(Ship): 商家将商品发出的动作
// - 取消(Cancel): 终止订单流程
public class Order {
private OrderId id;
private CustomerId customerId;
private List<OrderItem> items;
private OrderStatus status;
private Money totalAmount;
// 方法名使用业务术语而非技术术语
public void placeOrder() {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("只有草稿状态订单可下单");
}
validateItems();
calculateTotalAmount();
this.status = OrderStatus.PLACED;
// 发布领域事件
DomainEvents.publish(new OrderPlacedEvent(this.id, this.totalAmount));
}
public void pay(Payment payment) {
if (status != OrderStatus.PLACED) {
throw new IllegalStateException("只有已下单状态订单可支付");
}
if (payment.getAmount().lessThan(totalAmount)) {
throw new InsufficientPaymentException();
}
this.status = OrderStatus.PAID;
DomainEvents.publish(new OrderPaidEvent(this.id, payment));
}
}
统一语言的建立过程通常需要3至5次领域建模工作坊,业务专家和开发团队共同梳理领域知识,产出术语表和初步领域模型。术语表是活文档,随业务理解深入持续更新。
聚合根设计:一致性边界与不变量保护
聚合(Aggregate)是DDD的核心概念,是一组相关领域对象的集合,聚合根(Aggregate Root)是聚合的入口点,外部只能通过聚合根操作聚合内对象。聚合设计的关键原则:聚合内强一致性,聚合间最终一致性。
// 聚合根:Order
// 聚合内对象:OrderItem, ShippingAddress
// 聚合不变量:订单总金额 = 所有订单项金额之和
public class Order extends AggregateRoot {
private OrderId id;
private List<OrderItem> items;
private ShippingAddress address;
private OrderStatus status;
private Money totalAmount;
// 聚合根负责维护不变量
// 外部不能直接操作OrderItem,必须通过Order
public void addItem(Product product, int quantity) {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("非草稿订单不可修改");
}
OrderItem item = new OrderItem(product, quantity);
items.add(item);
recalculateTotal();
}
public void removeItem(OrderItemId itemId) {
items.removeIf(item -> item.getId().equals(itemId));
recalculateTotal();
}
private void recalculateTotal() {
this.totalAmount = items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
public OrderId getId() {
return id;
}
}
// 聚合内对象不持有其他聚合的引用,只持有ID
public class OrderItem {
private OrderItemId id;
private ProductId productId; // 引用其他聚合的ID
private int quantity;
private Money unitPrice;
public Money getSubtotal() {
return unitPrice.multiply(quantity);
}
}
聚合设计的一个常见错误是聚合过大:将Order、Product、Customer、Inventory放在同一聚合中。正确做法是将每个独立实体设计为单独聚合,通过ID引用关联。Order聚合只持有ProductId而非Product对象,Product的变更通过领域事件通知Order聚合。
限界上下文与上下文映射模式
限界上下文(Bounded Context)是DDD的战略设计层面概念,为领域模型划定明确的边界。同一个业务概念在不同上下文中含义不同:例如”Product”在商品上下文中表示可售卖的商品,在库存上下文中表示库存单位,在物流上下文中表示待发货的包裹。
// 限界上下文划分示例
// 1. 商品上下文 (Product Context)
// - Product: 商品信息、价格、描述
// - 关注点: 商品展示和搜索
// 2. 订单上下文 (Order Context)
// - Order: 订单、订单项
// - Product (精简版): 仅包含ProductId和下单时价格快照
// - 关注点: 交易流程管理
// 3. 库存上下文 (Inventory Context)
// - StockItem: 库存数量、仓库位置
// - Product (精简版): 仅包含ProductId和SKU
// - 关注点: 库存扣减和补货
// 4. 物流上下文 (Shipping Context)
// - Shipment: 发货单、物流轨迹
// - Product (精简版): 仅包含ProductId和数量
// - 关注点: 配送和追踪
// 上下文映射:防腐层(Anti-Corruption Layer)
// 订单上下文访问商品上下文时,通过ACL转换模型
public class ProductACLAdapter {
private final ProductApiClient productApi;
public OrderProductInfo toOrderProduct(String productId) {
ProductDTO dto = productApi.getProduct(productId);
// 转换为订单上下文所需的精简模型
return new OrderProductInfo(
dto.getId(),
dto.getName(),
dto.getPrice() // 价格快照
);
}
}
上下文映射的常见模式包括:防腐层(ACL)隔离外部模型污染、共享内核(Shared Kernel)在两个上下文间共享部分模型、客户/供应商(Customer/Supplier)定义上下游依赖关系、开放主机服务(OHS)+发布语言(PL)通过标准API集成。微服务架构中,每个限界上下文对应一个微服务,服务间通过API或消息队列集成。
事件溯源架构实现:事件存储与投影查询
事件溯源(Event Sourcing)将聚合状态变更记录为不可变事件序列,聚合当前状态通过回放事件重建。事件溯源的核心组件包括事件存储(Event Store)、事件总线(Event Bus)和投影(Projection):
// 事件定义
public abstract class DomainEvent {
private String aggregateId;
private long version;
private Instant occurredAt;
}
public class OrderPlacedEvent extends DomainEvent {
private String orderId;
private String customerId;
private BigDecimal totalAmount;
private List<OrderItemSnapshot> items;
}
public class OrderPaidEvent extends DomainEvent {
private String orderId;
private String paymentId;
private BigDecimal amount;
}
// 事件存储接口
public interface EventStore {
void append(String aggregateId, List<DomainEvent> events, long expectedVersion);
List<DomainEvent> load(String aggregateId);
List<DomainEvent> loadFrom(String aggregateId, long fromVersion);
}
// 聚合仓库 - 通过事件回放重建聚合
public class OrderRepository {
private final EventStore eventStore;
public Order findById(OrderId id) {
List<DomainEvent> events = eventStore.load(id.getValue());
if (events.isEmpty()) {
return null;
}
Order order = new Order();
for (DomainEvent event : events) {
order.apply(event);
}
return order;
}
public void save(Order order) {
List<DomainEvent> newEvents = order.getUncommittedEvents();
eventStore.append(
order.getId().getValue(),
newEvents,
order.getVersion() - newEvents.size()
);
newEvents.forEach(eventBus::publish);
}
}
// 事件投影 - 为查询优化建立物化视图
public class OrderListProjection {
private final JdbcTemplate jdbc;
@EventHandler
public void on(OrderPlacedEvent event) {
jdbc.update(
"INSERT INTO order_list_view (id, customer_id, total, status, created_at) " +
"VALUES (?, ?, ?, 'PLACED', ?)",
event.getOrderId(), event.getCustomerId(),
event.getTotalAmount(), event.getOccurredAt()
);
}
@EventHandler
public void on(OrderPaidEvent event) {
jdbc.update(
"UPDATE order_list_view SET status = 'PAID' WHERE id = ?",
event.getOrderId()
);
}
}
事件溯源与CQRS(Command Query Responsibility Segregation)天然配合:写操作通过聚合处理命令产生事件存储到Event Store,读操作从投影表查询物化视图。这种分离使得读写模型可以独立优化:写模型保证业务逻辑完整性,读模型针对不同查询场景建立多个投影表。事件溯源的工程挑战包括事件版本迁移(事件结构变更需要兼容历史数据)、快照优化(长事件序列回放性能)和最终一致性窗口处理(投影同步延迟)。在中小型系统中,事件溯源的复杂度可能超过收益,建议在需要完整审计轨迹或复杂状态回放的场景采用。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/ddd-ling-yu-qu-dong-she-ji-shi-zhan-ju-he-gen-xian-jie/