3. 领域驱动设计入门:实体、值对象、聚合、领域事件核心概念

领域驱动设计(Domain-Driven Design,DDD)是一套应对复杂业务系统的建模方法论。在分布式事务场景中,DDD 帮助我们划定事务边界、识别一致性单元。本章聚焦 DDD 的四大核心构造块:实体、值对象、聚合 和 领域事件,并解释它们如何与 Go 语言实现及分布式事务画布对齐。

3.1 实体(Entity):拥有唯一标识的领域对象

实体是具有唯一标识且可变的领域对象。两个实体即使属性完全相同,只要标识不同,就是不同的对象。在分布式系统中,实体的 ID 通常由 UUID、雪花算法或数据库序列生成。

关键特征:
  • 拥有全局或局部唯一的标识符(ID)。
  • 生命周期内状态可以变化,但标识不变。
  • 通过 ID 相等性判断,而非属性值。

Go 实现示例:

// Order 是订单实体,使用 UUID 作为唯一标识
type Order struct {
    ID         string    // 唯一标识,不可变
    CustomerID string
    Items      []OrderItem
    Status     OrderStatus
    CreatedAt  time.Time
}

// SameIdentityAs 比较两个实体的标识是否相同
func (o *Order) SameIdentityAs(other *Order) bool {
    return o.ID == other.ID
}

3.2 值对象(Value Object):描述性、不可变、无标识

值对象通过属性值来定义自身,没有概念上的唯一标识。两个值对象如果所有属性都相同,则视为同一个对象。值对象通常是不可变的,修改时返回新实例。

对比维度 实体(Entity) 值对象(Value Object)
标识 有唯一标识 无标识,由属性定义
可变性 可变 不可变
相等性 ID 相等 所有属性相等
生命周期 独立生命周期 依附于实体
分布式事务影响 事务边界核心 随实体一起持久化

Go 实现示例:

// Money 是值对象,表示金额
type Money struct {
    amount   decimal.Decimal
    currency string
}

// NewMoney 工厂方法,返回不可变实例
func NewMoney(amount decimal.Decimal, currency string) Money {
    return Money{amount: amount, currency: currency}
}

// Add 返回新的 Money 实例,不修改原对象
func (m Money) Add(other Money) (Money, error) {
    if m.currency != other.currency {
        return Money{}, errors.New("currency mismatch")
    }
    return NewMoney(m.amount.Add(other.amount), m.currency), nil
}

// Equals 比较两个值对象是否相等
func (m Money) Equals(other Money) bool {
    return m.amount.Equal(other.amount) && m.currency == other.currency
}

3.3 聚合(Aggregate):事务一致性的边界

聚合是 DDD 中最重要的构造块,它定义了事务一致性的边界。一个聚合是一组相关对象的集群,外部只能通过聚合根(Aggregate Root)来访问内部对象。在分布式事务画布中,一个聚合对应一个本地事务边界。

聚合设计三原则:
  1. 聚合根保护不变条件:聚合根是唯一对外接口,内部对象不能直接暴露。
  2. 跨聚合最终一致性:不同聚合之间通过领域事件实现最终一致,而非强事务。
  3. 一个事务只修改一个聚合:这是分布式事务拆分的基本单元。

Go 实现示例:

// OrderAggregate 是订单聚合根
type OrderAggregate struct {
    order   *Order        // 聚合根实体
    items   []OrderItem   // 聚合内部实体
    history []OrderEvent  // 领域事件列表
}

// NewOrderAggregate 创建聚合,确保不变条件
func NewOrderAggregate(customerID string) (*OrderAggregate, error) {
    if customerID == "" {
        return nil, errors.New("customer ID is required")
    }
    agg := &OrderAggregate{
        order: &Order{
            ID:         uuid.New().String(),
            CustomerID: customerID,
            Status:     OrderStatusPending,
            CreatedAt:  time.Now(),
        },
    }
    // 记录领域事件
    agg.addEvent(OrderCreated{OrderID: agg.order.ID})
    return agg, nil
}

// AddItem 通过聚合根修改内部状态
func (agg *OrderAggregate) AddItem(productID string, quantity int, price Money) error {
    if agg.order.Status != OrderStatusPending {
        return errors.New("can only add items to pending order")
    }
    item := OrderItem{
        ProductID: productID,
        Quantity:  quantity,
        Price:     price,
    }
    agg.items = append(agg.items, item)
    agg.addEvent(ItemAddedToOrder{OrderID: agg.order.ID, ProductID: productID})
    return nil
}

// addEvent 内部方法,记录领域事件
func (agg *OrderAggregate) addEvent(event OrderEvent) {
    agg.history = append(agg.history, event)
}
聚合边界与事务一致性 聚合 A:订单聚合 聚合根:OrderAggregate Order(实体) OrderItem(实体) Money(值对象) Address(值对象) 事务边界:强一致性 一个事务只修改一个聚合 聚合 B:库存聚合 聚合根:InventoryAggregate Product(实体) StockLevel(值对象) 事务边界:强一致性 一个事务只修改一个聚合 领域事件 → 最终一致性 每个聚合是一个独立的事务边界,跨聚合通信通过领域事件实现最终一致性

3.4 领域事件(Domain Event):跨聚合的通信契约

领域事件是已经发生的事实,用于聚合之间的异步通信。在分布式事务画布中,领域事件是最终一致性的核心载体。当一个聚合完成状态变更后,发布领域事件,其他聚合订阅并做出响应。

领域事件设计要点:
  • 事件名称使用过去时(如 OrderCreated、PaymentCompleted)。
  • 事件包含足够的数据供订阅者处理,但不包含敏感信息。
  • 事件是不可变的,发布后不可修改。
  • 事件通常携带事件 ID 和 时间戳,用于去重和追踪。

Go 实现示例:

// OrderCreated 领域事件:订单已创建
type OrderCreated struct {
    EventID    string    `json:"event_id"`
    OrderID    string    `json:"order_id"`
    CustomerID string    `json:"customer_id"`
    Total      Money     `json:"total"`
    OccurredAt time.Time `json:"occurred_at"`
}

// NewOrderCreated 创建领域事件实例
func NewOrderCreated(orderID, customerID string, total Money) OrderCreated {
    return OrderCreated{
        EventID:    uuid.New().String(),
        OrderID:    orderID,
        CustomerID: customerID,
        Total:      total,
        OccurredAt: time.Now(),
    }
}

// EventBus 事件总线接口,用于发布和订阅领域事件
type EventBus interface {
    Publish(event interface{}) error
    Subscribe(eventType string, handler func(event interface{})) error
}

// 库存聚合订阅订单创建事件
func SubscribeOrderCreated(bus EventBus, inventoryService *InventoryService) {
    bus.Subscribe("OrderCreated", func(event interface{}) {
        e := event.(OrderCreated)
        // 异步扣减库存,实现最终一致性
        inventoryService.ReserveStock(e.OrderID, e.CustomerID)
    })
}
领域事件驱动:跨聚合最终一致性 订单聚合 1. 创建订单 2. 发布 OrderCreated 事件总线 OrderCreated 异步分发 库存聚合 3. 订阅事件 4. 扣减库存 通知聚合 5. 订阅事件 6. 发送确认邮件 一个领域事件可以被多个聚合订阅,每个聚合独立处理,实现最终一致性

3.5 四大构造块在分布式事务画布中的角色

构造块 分布式事务角色 事务边界 一致性模型
实体 事务参与者,状态变更的主体 聚合内部 强一致性
值对象 事务数据载体,不可变快照 随实体持久化 强一致性
聚合 事务边界单元,ACID 保证 聚合根 强一致性
领域事件 跨聚合通信契约,异步协调 聚合之间 最终一致性
实战要点:
  • 在 Go 中,聚合根通常是一个结构体,内部持有实体和值对象的引用。
  • 领域事件使用接口或结构体定义,通过事件总线(如 RabbitMQ、Kafka 或内存通道)发布。
  • 分布式事务画布中,每个聚合对应一个本地事务,跨聚合操作通过 Saga 或事件驱动实现。
  • 值对象在 Go 中推荐使用不可变设计,所有修改方法返回新实例。