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
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"}
}
聚合设计原则:
- 小聚合优先:只包含必须保持一致的实体和值对象。
- 通过聚合根访问:禁止外部直接修改聚合内的子对象。
- 一次事务只修改一个聚合:跨聚合一致性应通过领域事件最终一致。
3.4 领域事件(Domain Event):记录已发生的业务事实
领域事件表示领域中已经发生的、对业务有意义的事件。它是聚合之间解耦通信的核心手段,也是事件溯源和 CQRS 的基础。领域事件通常以过去时态命名,且不可变。
// 领域事件:订单已支付
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 { ... } |
核心原则总结:
- 实体是 API 资源的身份锚点,通过 ID 路由。
- 值对象是 API 契约中的数据传输单元,不可变且可比较。
- 聚合是 API 操作的事务边界,保证请求内数据一致性。
- 领域事件是 API 间异步通信的契约,实现最终一致性。