DDD领域驱动设计实战:聚合根、限界上下文与事件溯源落地

领域驱动设计(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/

(0)
小编小编
上一篇 7小时前
下一篇 7小时前

相关推荐