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

领域驱动设计(DDD)提供了一套用于构建复杂业务软件的战术模式。在 Go 语言中,这些模式帮助我们明确领域边界、封装业务规则,并生成可测试、可演化的 API 契约。本章聚焦于四个最核心的构造块:实体(Entity)、值对象(Value Object)、聚合(Aggregate)和领域事件(Domain Event)。

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

实体是领域模型中具有连续性和身份的对象。即使其属性随时间变化,只要标识不变,它仍然是同一个对象。在 Go 中,实体通常通过一个不可变的 ID 字段来区分。

关键特征:
  • 拥有唯一标识(如 UUID、业务主键)。
  • 可变性:属性可以修改,但标识不变。
  • 相等性:两个实体如果 ID 相同,则视为同一个实体。
// 实体:用户
type User struct {
    ID        string    // 唯一标识,不可变
    Name      string    // 可变属性
    Email     string    // 可变属性
    CreatedAt time.Time
}

// 通过 ID 判断相等性
func (u User) SameIdentityAs(other User) bool {
    return u.ID == other.ID
}

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

值对象用于描述实体的属性或特征,本身没有独立标识。两个值对象如果所有属性都相同,则视为相等。值对象必须是不可变的,修改即替换。

对比维度 实体(Entity) 值对象(Value Object)
标识 有唯一 ID 无标识
可变性 可变 不可变
相等性 ID 相等 所有属性相等
生命周期 独立存在 依附于实体
Go 实现 struct + ID 字段 struct + 值语义方法
// 值对象:地址
type Address struct {
    Street   string
    City     string
    ZipCode  string
}

// 不可变:返回新实例
func (a Address) WithStreet(newStreet string) Address {
    return Address{
        Street:   newStreet,
        City:     a.City,
        ZipCode:  a.ZipCode,
    }
}

// 值相等性
func (a Address) Equals(other Address) bool {
    return a.Street == other.Street &&
        a.City == other.City &&
        a.ZipCode == other.ZipCode
}
Go 实践提示:值对象应避免暴露指针接收器方法。所有“修改”操作都应返回新实例,确保不可变性。在 API 契约中,值对象通常映射为请求/响应中的嵌套结构体。

3.3 聚合(Aggregate):一致性边界与事务单元

聚合是一组相关实体和值对象的一致性边界。聚合根(Aggregate Root)是外部访问聚合内对象的唯一入口。聚合保证边界内的业务规则始终一致,是事务的原子单元。

聚合边界与聚合根模式 聚合边界(事务一致性单元) 聚合根:Order OrderID, Status, TotalAmount 实体:OrderItem ItemID, ProductID, Quantity 值对象:Money Amount, Currency 值对象:ShippingAddress Street, City, ZipCode 实体:Payment PaymentID, Method, Status 外部只能通过 Order 聚合根访问内部实体和值对象,保证数据一致性
// 聚合根:Order
type Order struct {
    ID              string
    Items           []OrderItem
    ShippingAddress Address
    Payment         Payment
    Total           Money
    Status          OrderStatus
}

// 业务方法:添加订单项(保证一致性)
func (o *Order) AddItem(productID string, qty int, price Money) error {
    if o.Status != OrderStatusPending {
        return ErrOrderNotModifiable
    }
    item := OrderItem{
        ItemID:    uuid.New().String(),
        ProductID: productID,
        Quantity:  qty,
        Price:     price,
    }
    o.Items = append(o.Items, item)
    o.recalculateTotal() // 内部重新计算总额
    return nil
}

// 私有方法:重新计算总额
func (o *Order) recalculateTotal() {
    var total float64
    for _, item := range o.Items {
        total += item.Price.Amount * float64(item.Quantity)
    }
    o.Total = Money{Amount: total, Currency: "CNY"}
}
聚合设计原则:
  1. 小聚合优先:只包含必须保持一致的实体和值对象。
  2. 通过聚合根访问:禁止外部直接修改聚合内的子对象。
  3. 一次事务只修改一个聚合:跨聚合一致性应通过领域事件最终一致。

3.4 领域事件(Domain Event):记录已发生的业务事实

领域事件表示领域中已经发生的、对业务有意义的事件。它是聚合之间解耦通信的核心手段,也是事件溯源和 CQRS 的基础。领域事件通常以过去时态命名,且不可变。

领域事件驱动流程 源聚合:Order 订单已支付 领域事件总线 OrderPaidEvent 目标聚合:Inventory 扣减库存 目标聚合:Notification 发送支付成功通知 目标聚合:Analytics 记录支付统计 领域事件实现聚合间最终一致性,避免跨聚合分布式事务 事件应包含所有必要数据,使消费者无需回查源聚合
// 领域事件:订单已支付
type OrderPaidEvent struct {
    EventID   string    // 事件唯一标识
    OrderID   string    // 关联聚合根 ID
    PaidAt    time.Time // 事件发生时间
    Amount    Money     // 支付金额(值对象)
    PaymentID string    // 支付记录 ID
}

// 事件发布接口
type EventPublisher interface {
    Publish(event interface{}) error
}

// 在聚合根中发布事件
func (o *Order) MarkAsPaid(paymentID string, paidAt time.Time) error {
    if o.Status != OrderStatusPending {
        return ErrInvalidOrderStatus
    }
    o.Status = OrderStatusPaid
    o.Payment = Payment{PaymentID: paymentID, Status: PaymentStatusSuccess}

    // 发布领域事件
    event := OrderPaidEvent{
        EventID:   uuid.New().String(),
        OrderID:   o.ID,
        PaidAt:    paidAt,
        Amount:    o.Total,
        PaymentID: paymentID,
    }
    o.addDomainEvent(event) // 将事件暂存到聚合内,事务提交时统一发布
    return nil
}
Go 实现要点:
  • 领域事件使用 struct 定义,所有字段导出且不可变(无指针接收器修改方法)。
  • 事件应包含 EventID 和 OccurredAt 等元数据字段。
  • 推荐在基础设施层通过消息队列(如 RabbitMQ、Kafka)或事件存储实现跨服务发布。
  • 事件命名使用过去时态:OrderCreated、InventoryReserved、PaymentFailed。

3.5 四者关系与 API 契约映射

DDD 概念 API 契约中的体现 Go 代码示例
实体(Entity) 资源路径中的主资源,如 /orders/{orderId} type Order struct { ID string ... }
值对象(Value Object) 请求/响应中的嵌套结构体,如 address、money type Address struct { Street string ... }
聚合(Aggregate) API 端点对应聚合根操作,如 POST /orders/{id}/items func (o *Order) AddItem(...) error
领域事件(Domain Event) Webhook 回调、事件流端点、异步消息契约 type OrderPaidEvent struct { ... }
核心原则总结:
  1. 实体是 API 资源的身份锚点,通过 ID 路由。
  2. 值对象是 API 契约中的数据传输单元,不可变且可比较。
  3. 聚合是 API 操作的事务边界,保证请求内数据一致性。
  4. 领域事件是 API 间异步通信的契约,实现最终一致性。