4. 领域事件的定义与建模:事件命名规范、事件属性设计、事件版本控制策略

领域事件是事件驱动架构与事件溯源的核心数据载体。一个设计良好的领域事件,不仅需要准确表达“发生了什么”,还要具备跨版本、跨系统的兼容能力。本节从命名规范、属性设计、版本控制三个维度,给出 Go 语言下的可落地建模方案。

4.1 事件命名规范:用过去时态表达业务事实

领域事件的命名应遵循“业务动作 + 过去时态”原则,使事件名称本身就是一个完整的业务陈述。推荐使用 动词过去分词 作为事件类型后缀。

规范维度 推荐做法 反例
时态 使用过去分词(-ed / -en) OrderCreated ✅ → OrderCreate ❌
业务语义 包含聚合根类型 + 动作 OrderShipped ✅ → ShipEvent ❌
命名空间 按限界上下文分组 ordering.OrderCreated ✅ → Event1 ❌
大小写 PascalCase(Go 导出类型) InventoryAdjusted ✅ → inventory_adjusted ❌
命名规范速查表:
  • OrderCreated — 订单已创建
  • OrderLineItemAdded — 订单行项目已添加
  • PaymentCaptured — 支付已捕获
  • InventoryReservationFailed — 库存预留失败

4.2 事件属性设计:最小化、不可变、可溯源

领域事件的属性应遵循 最小化原则:只携带触发该事件所必需的业务数据,不包含派生或可计算字段。所有属性在事件创建后不可修改。

4.2.1 标准属性结构

// EventMeta 事件元数据,所有领域事件共用
type EventMeta struct {
    EventID      string    // 全局唯一事件ID (UUID v7)
    AggregateID  string    // 聚合根ID
    AggregateType string   // 聚合根类型名
    EventType    string    // 事件类型全名
    OccurredAt   time.Time // 事件发生时间 (UTC)
    Version      int       // 事件版本号
}

// OrderCreated 订单已创建事件
type OrderCreated struct {
    Meta      EventMeta
    OrderID   string
    CustomerID string
    TotalAmount Money
    Items     []OrderLineItem
}

type OrderLineItem struct {
    ProductID string
    Quantity  int
    UnitPrice Money
}

4.2.2 属性设计原则

原则 说明 Go 实现建议
不可变性 事件一旦创建,属性不可修改 使用值对象(struct 导出字段但无 setter)
最小化 只包含触发事件所必需的数据 不包含计算字段(如订单总价)
可序列化 所有属性必须可序列化为 JSON/Protobuf 避免使用 chan、func、interface{}
业务语义 属性名使用业务术语,非技术术语 CustomerID ✅ → UserID ❌
避坑提示:不要在事件中嵌入整个聚合根对象。事件是“发生了什么”的记录,不是“当前状态”的快照。嵌入完整聚合根会导致事件体积膨胀、版本耦合加剧。

4.3 事件版本控制策略:向前兼容与模式演化

随着业务演进,事件结构必然发生变化。版本控制策略的目标是:旧版本消费者不崩溃,新版本消费者能获取完整信息。

4.3.1 版本号策略

推荐使用 整数递增版本号,每个事件类型独立维护版本。版本号从 1 开始,每次结构变更(新增/删除/重命名字段)时递增。

// v1 版本
type OrderCreatedV1 struct {
    Meta       EventMeta
    OrderID    string
    CustomerID string
    Items      []OrderLineItem
}

// v2 版本:新增 ShippingAddress 字段
type OrderCreatedV2 struct {
    Meta            EventMeta
    OrderID         string
    CustomerID      string
    Items           []OrderLineItem
    ShippingAddress Address  // 新增字段
}

4.3.2 向前兼容规则

变更类型 是否兼容 处理方式
新增可选字段 ✅ 兼容 旧版本反序列化时忽略新字段
新增必填字段 ⚠️ 不兼容 必须升级事件版本号
删除字段 ⚠️ 不兼容 必须升级事件版本号
重命名字段 ⚠️ 不兼容 必须升级事件版本号
修改字段类型 ⚠️ 不兼容 必须升级事件版本号

4.3.3 Go 中的版本化序列化策略

推荐使用 带版本标记的包装结构 进行序列化,避免使用继承或接口断言。

// Envelope 事件信封,携带版本信息
type Envelope struct {
    EventType string          `json:"event_type"`
    Version   int             `json:"version"`
    Data      json.RawMessage `json:"data"`
}

// 反序列化示例
func UnmarshalEvent(raw []byte) (interface{}, error) {
    var env Envelope
    if err := json.Unmarshal(raw, &env); err != nil {
        return nil, err
    }

    switch env.EventType {
    case "OrderCreated":
        switch env.Version {
        case 1:
            var evt OrderCreatedV1
            json.Unmarshal(env.Data, &evt)
            return &evt, nil
        case 2:
            var evt OrderCreatedV2
            json.Unmarshal(env.Data, &evt)
            return &evt, nil
        }
    }
    return nil, fmt.Errorf("unknown event: %s v%d", env.EventType, env.Version)
}

4.4 事件建模流程:从业务事件到 Go 结构体

下图展示了从业务事件识别到最终 Go 代码的完整建模流程:

领域事件建模流程 1. 识别业务事件 事件风暴 / 业务分析 2. 定义事件名称 过去分词 + 聚合根 3. 设计属性集 最小化 + 不可变 4. 定版本 v1 起步 5. 编写 Go 结构体 嵌入 EventMeta 6. 实现序列化 / 反序列化 Envelope + 版本路由 7. 注册到事件总线

4.5 事件版本演化实战:一个完整的 Go 示例

假设订单系统最初只有国内地址,后续需要支持国际地址。以下是版本演化的完整过程:

// v1 版本:仅国内地址
type OrderShippedV1 struct {
    Meta        EventMeta
    OrderID     string
    Address     string  // "北京市朝阳区..."
}

// v2 版本:支持国际地址,新增结构化地址
type Address struct {
    Street    string
    City      string
    Country   string  // ISO 3166-1 alpha-2
    PostalCode string
}

type OrderShippedV2 struct {
    Meta        EventMeta
    OrderID     string
    Address     Address  // 替换原 string 类型
    OldAddress  string   // 保留旧字段用于兼容
}

// 版本升级器:将 v1 事件升级为 v2
func UpgradeOrderShippedV1ToV2(v1 *OrderShippedV1) *OrderShippedV2 {
    return &OrderShippedV2{
        Meta:    v1.Meta,
        OrderID: v1.OrderID,
        Address: Address{
            Street: v1.Address,
            City:   "Unknown",
            Country: "CN",
        },
        OldAddress: v1.Address,
    }
}
最佳实践:当事件版本升级时,建议同时保留旧版本的序列化器,并提供一个“版本升级器”(Upgrader)函数。这样事件溯源存储中的旧事件可以在读取时按需升级,无需重写历史数据。

4.6 事件建模检查清单

  • 命名:是否使用过去分词?是否包含聚合根类型?
  • 属性:是否只包含必要业务数据?是否所有属性都不可变?
  • 版本:是否每个事件类型都有独立版本号?是否实现了版本路由?
  • 序列化:是否使用 Envelope 包装?是否支持向前兼容?
  • 测试:是否编写了旧版本事件的反序列化测试?