4. 领域驱动设计基础:实体、值对象、聚合、领域事件、限界上下文的核心概念
领域驱动设计(Domain-Driven Design,DDD)为复杂业务系统的建模提供了一套严谨的战术模式。在 Go 后端架构中,正确运用这些模式是划分服务边界、设计发布拓扑的前提。本章将逐一拆解 DDD 的五个核心构造块:实体、值对象、聚合、领域事件和限界上下文,并说明它们在 Go 代码中的典型映射。
4.1 实体(Entity)
实体是具有唯一标识且生命周期持续变化的对象。两个实体即使所有属性相同,只要 ID 不同,就是不同的对象。
- 核心特征:拥有标识符(ID),可变(Mutable),通过 ID 进行相等性比较。
- Go 实现要点:通常使用 struct 表示,ID 字段单独定义类型,避免与基础类型混淆。
Go 实体示例:
// UserId 是用户实体的标识符类型
type UserId string
type User struct {
ID UserId
Name string
Email string
Version int // 乐观锁版本号
CreatedAt time.Time
UpdatedAt time.Time
}
// 通过 ID 比较两个实体是否相同
func (u *User) Equals(other *User) bool {
return u.ID == other.ID
}
4.2 值对象(Value Object)
值对象通过属性值来定义自身,没有唯一标识,且通常是不可变(Immutable)的。两个值对象如果所有属性相同,则视为同一个对象。
- 核心特征:无 ID、不可变、通过属性值比较、可被共享和替换。
- Go 实现要点:使用 struct 且不暴露修改方法;通过 DeepEqual 或自定义 Equals 方法比较。
设计提示:在 Go 中,值对象通常不包含指针或引用类型字段,以确保值语义的纯粹性。对于金额、地址、坐标等概念,优先建模为值对象。
// Address 是一个典型的值对象
type Address struct {
Province string
City string
District string
Detail string
}
// 值对象比较:所有字段相等即视为相同
func (a Address) Equals(other Address) bool {
return a.Province == other.Province &&
a.City == other.City &&
a.District == other.District &&
a.Detail == other.Detail
}
4.3 聚合(Aggregate)
聚合是一组相关对象的一致性边界。外部对象只能通过聚合根(Aggregate Root)来访问聚合内部的实体和值对象。聚合保证了业务不变量的强一致性。
| 概念 | 说明 | Go 映射 |
|---|---|---|
| 聚合根 | 聚合中唯一能被外部引用的实体 | 顶层 struct,包含全局唯一 ID |
| 内部实体 | 属于聚合内部,只能通过聚合根访问 | 小写字段或包内私有 struct |
| 值对象 | 聚合内部的属性集合 | 不可变 struct,嵌入或作为字段 |
| 一致性边界 | 事务范围内保证所有内部对象状态一致 | Repository 以聚合为单位进行持久化 |
常见误区:不要将数据库表的一对多关系直接映射为聚合关系。聚合边界应由业务不变性决定,而非数据关联。过大的聚合会导致性能瓶颈和并发冲突。
// Order 是聚合根
type Order struct {
ID OrderId
CustomerId UserId
Items []OrderItem // 内部实体,不对外暴露直接修改
Status OrderStatus
Version int
}
// OrderItem 是聚合内部的实体,没有全局 ID
type OrderItem struct {
ProductId string
Quantity int
UnitPrice Money
}
// 通过聚合根的方法修改内部状态
func (o *Order) AddItem(productId string, qty int, price Money) error {
// 业务校验:检查库存、重复商品等
o.Items = append(o.Items, OrderItem{
ProductId: productId,
Quantity: qty,
UnitPrice: price,
})
return nil
}
4.4 领域事件(Domain Event)
领域事件是领域中发生的、业务人员关心的事实。它用于在聚合之间传递状态变更通知,实现最终一致性。在分布式系统中,领域事件是服务间异步协作的核心载体。
- 特征:过去时命名(如 OrderCreated)、包含事件 ID 和发生时间、不可变。
- Go 实现:通常定义为 struct,包含事件元数据和业务载荷。
// OrderCreated 领域事件
type OrderCreated struct {
EventID string `json:"event_id"`
OccurredAt time.Time `json:"occurred_at"`
OrderID string `json:"order_id"`
CustomerID string `json:"customer_id"`
TotalAmount float64 `json:"total_amount"`
}
// 在聚合根方法中发布事件
func (o *Order) Create() ([]interface{}, error) {
// ... 业务逻辑 ...
event := OrderCreated{
EventID: uuid.New().String(),
OccurredAt: time.Now(),
OrderID: string(o.ID),
CustomerID: string(o.CustomerId),
TotalAmount: o.CalculateTotal(),
}
return []interface{}{event}, nil
}
发布策略:在 Go 后端中,领域事件通常在 Repository 的 Save 方法内,与聚合状态变更一起原子性地持久化到事件存储或消息队列中,避免“发事件”与“改状态”不一致。
4.5 限界上下文(Bounded Context)
限界上下文是 DDD 中最宏观的战略模式。它定义了模型应用的明确边界。在每个限界上下文内部,统一语言(Ubiquitous Language)和模型是自洽的;不同上下文之间的模型可以有不同的含义和结构。
| 维度 | 说明 | Go 微服务映射 |
|---|---|---|
| 模型边界 | 每个上下文拥有独立的领域模型 | 独立的 Go module 或 package |
| 统一语言 | 团队内部使用相同的术语沟通 | 代码中的命名与业务术语一致 |
| 通信方式 | 上下文之间通过事件或 API 协作 | gRPC / HTTP / 消息队列 |
| 数据存储 | 每个上下文拥有独立的数据库或 schema | 独立的数据库实例或表前缀 |
4.6 核心概念对比
下表总结了五个核心概念在标识、可变性、生命周期和比较方式上的关键差异:
| 概念 | 唯一标识 | 可变性 | 生命周期 | 相等性比较 |
|---|---|---|---|---|
| 实体 | 有(ID) | 可变 | 持续变化 | 通过 ID |
| 值对象 | 无 | 不可变 | 无(瞬态) | 通过所有属性 |
| 聚合 | 聚合根有 ID | 内部可变,外部不可见 | 事务边界内一致 | 通过聚合根 ID |
| 领域事件 | 有(EventID) | 不可变 | 发布即完成 | 通过 EventID |
| 限界上下文 | 有(上下文名称) | 模型独立演进 | 团队/服务生命周期 | 不适用 |