2. DDD 核心概念:通用语言、限界上下文、实体、值对象、聚合、领域事件
领域驱动设计(DDD)提供了一套精密的战术建模工具,用于将复杂的业务逻辑映射到软件实现中。本章将深入剖析 DDD 的六大核心概念,并结合 Go 语言的实现特性,展示如何在微服务架构中落地这些模式。
2.1 通用语言(Ubiquitous Language)
通用语言是 DDD 的基石,它要求开发团队与领域专家使用同一套术语进行沟通,并将这些术语精确地反映在代码、文档和测试中。
- 目标:消除“业务说一套,技术做一套”的鸿沟。
- 载体:代码中的类名、方法名、变量名;事件风暴中的贴纸;用户故事中的描述。
- 原则:术语必须精确、无歧义,且在整个限界上下文中保持一致。
实践建议:在 Go 项目中,包名、结构体名、接口名应直接反映通用语言。例如,使用
package order 而非 package biz,使用 type OrderStatus string 而非 type Status int。
2.2 限界上下文(Bounded Context)
限界上下文是 DDD 的战略核心,它划定了模型应用的边界。每个上下文内部有独立的通用语言和模型,上下文之间通过防腐层(ACL)或开放主机服务(OHS)进行通信。
| 特性 | 说明 | 微服务映射 |
|---|---|---|
| 边界 | 定义模型适用的范围 | 一个限界上下文通常对应一个微服务 |
| 自治 | 内部模型独立演化 | 服务可独立部署、独立数据库 |
| 通信 | 通过事件或API交互 | gRPC、消息队列、REST |
| 冲突 | 不同上下文中同一术语含义不同 | “用户”在订单上下文和CRM上下文中属性不同 |
关键认知:限界上下文是微服务拆分的首要依据。错误的边界划分会导致“分布式单体”或过度通信。
2.3 实体(Entity)
实体是具有唯一标识且会随时间改变状态的对象。在 Go 中,实体通常通过结构体实现,并显式管理其标识。
// 订单实体:具有唯一标识 OrderID,状态会变化
type Order struct {
ID OrderID // 值对象作为标识
CustomerID CustomerID
Items []OrderItem
Status OrderStatus
CreatedAt time.Time
UpdatedAt time.Time
}
// 实体行为:封装业务逻辑
func (o *Order) Submit() error {
if o.Status != OrderStatusPending {
return ErrInvalidOrderStatus
}
o.Status = OrderStatusSubmitted
o.UpdatedAt = time.Now()
// 发布领域事件
o.Events = append(o.Events, OrderSubmittedEvent{OrderID: o.ID})
return nil
}
常见误区:不要将数据库主键(自增ID)直接作为实体标识。实体标识应具有业务含义或使用 UUID,确保在分布式环境下唯一。
2.4 值对象(Value Object)
值对象通过属性值来定义,没有唯一标识,且不可变。在 Go 中,值对象通常不包含指针接收器方法,并通过构造函数保证不变性。
| 特性 | 实体 | 值对象 |
|---|---|---|
| 标识 | 有唯一标识 | 无标识,由属性值定义 |
| 可变性 | 可变 | 不可变 |
| 相等性 | 标识相等 | 所有属性相等 |
| 生命周期 | 独立生命周期 | 依附于实体 |
| Go 实现 | struct + 指针接收器 | struct + 值接收器 + 工厂函数 |
// 值对象:地址,不可变
type Address struct {
Street string
City string
ZipCode string
Province string
}
// 工厂函数:保证创建时数据有效
func NewAddress(street, city, zipCode, province string) (Address, error) {
if street == "" || city == "" {
return Address{}, ErrInvalidAddress
}
return Address{
Street: street,
City: city,
ZipCode: zipCode,
Province: province,
}, nil
}
// 值对象相等性比较
func (a Address) Equals(other Address) bool {
return a.Street == other.Street &&
a.City == other.City &&
a.ZipCode == other.ZipCode &&
a.Province == other.Province
}
2.5 聚合(Aggregate)
聚合是一组相关对象的集群,外部只能通过聚合根(Aggregate Root)访问内部对象。聚合保证了数据的一致性和不变性。
- 聚合根:唯一能被外部引用的实体,负责协调内部所有操作。
- 边界:聚合内部对象不能从外部直接修改,必须通过聚合根。
- 事务范围:一个事务只修改一个聚合,避免跨聚合的事务。
- 不变条件:聚合根负责维护内部所有对象的不变规则。
// 聚合根:订单聚合
type Order struct {
ID OrderID
Items []OrderItem // 内部实体
Shipping Address // 值对象
Status OrderStatus
Events []DomainEvent // 待发布的领域事件
}
// 聚合根方法:添加订单项
func (o *Order) AddItem(productID ProductID, quantity int, unitPrice Money) error {
if o.Status != OrderStatusPending {
return ErrCannotModifySubmittedOrder
}
if quantity <= 0 {
return ErrInvalidQuantity
}
// 检查是否已存在相同商品
for i, item := range o.Items {
if item.ProductID == productID {
o.Items[i].Quantity += quantity
o.Items[i].UnitPrice = unitPrice
return nil
}
}
o.Items = append(o.Items, OrderItem{
ProductID: productID,
Quantity: quantity,
UnitPrice: unitPrice,
})
return nil
}
// 聚合根方法:计算总价
func (o *Order) TotalAmount() Money {
total := Money{Amount: 0, Currency: "CNY"}
for _, item := range o.Items {
total = total.Add(item.Subtotal())
}
return total
}
设计原则:聚合应该尽量小,只包含必须保持一致的属性。过大的聚合会导致性能问题和并发冲突。推荐一个聚合不超过 5-7 个内部对象。
2.6 领域事件(Domain Event)
领域事件表示领域中发生的、领域专家关心的重要事件。它用于解耦聚合之间的通信,是实现事件驱动架构的核心。
| 要素 | 说明 | Go 实现示例 |
|---|---|---|
| 事件名称 | 过去时,描述已发生的事情 | OrderSubmitted |
| 时间戳 | 事件发生的时间 | OccurredAt time.Time |
| 事件数据 | 携带必要的数据 | OrderID, CustomerID, TotalAmount |
| 事件ID | 唯一标识,用于幂等处理 | EventID string |
// 领域事件接口
type DomainEvent interface {
EventName() string
OccurredAt() time.Time
EventID() string
}
// 具体领域事件:订单已提交
type OrderSubmittedEvent struct {
OrderID OrderID
CustomerID CustomerID
TotalAmount Money
Timestamp time.Time
ID string
}
func (e OrderSubmittedEvent) EventName() string {
return "order.submitted"
}
func (e OrderSubmittedEvent) OccurredAt() time.Time {
return e.Timestamp
}
func (e OrderSubmittedEvent) EventID() string {
return e.ID
}
// 在聚合中收集并发布事件
func (o *Order) Submit() error {
// ... 业务逻辑 ...
o.Events = append(o.Events, OrderSubmittedEvent{
OrderID: o.ID,
CustomerID: o.CustomerID,
TotalAmount: o.TotalAmount(),
Timestamp: time.Now(),
ID: uuid.New().String(),
})
return nil
}
2.7 六大概念关系总结
这六个概念并非孤立存在,它们共同构成了 DDD 的战术建模体系。以下表格展示了它们之间的协作关系:
| 概念 | 核心作用 | 与其它概念的关系 | Go 实现要点 |
|---|---|---|---|
| 通用语言 | 统一沟通与代码 | 贯穿所有概念,是命名的基础 | 包名、类型名、方法名直接使用业务术语 |
| 限界上下文 | 划定模型边界 | 内部包含实体、值对象、聚合 | 每个上下文一个 Go module |
| 实体 | 有标识的可变对象 | 可作为聚合根或聚合内部对象 | struct + 显式 ID + 指针接收器方法 |
| 值对象 | 无标识的不可变对象 | 通常作为实体的属性 | struct + 值接收器 + 工厂函数 |
| 聚合 | 保证数据一致性 | 由聚合根(实体)和内部对象组成 | 聚合根暴露方法,内部对象不对外暴露 |
| 领域事件 | 解耦聚合间通信 | 由聚合产生,被其他聚合消费 | 接口 + 具体事件 struct + 事件总线 |
实战心法:在 Go 微服务中,建议先通过事件风暴识别限界上下文和聚合边界,再定义实体和值对象,最后设计领域事件。代码结构应严格遵循通用语言,让业务专家也能读懂代码。