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 代码的完整建模流程:
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 包装?是否支持向前兼容?
- 测试:是否编写了旧版本事件的反序列化测试?