1. 领域驱动设计(DDD)基础:实体、值对象、聚合、领域事件、限界上下文概念
领域驱动设计(Domain-Driven Design,简称 DDD)是一种软件设计方法论,强调以业务领域为核心,通过通用语言(Ubiquitous Language)将业务概念精确映射到代码模型中。在构建复杂业务系统时,DDD 提供了一套清晰的概念边界和建模工具,帮助团队避免技术实现与业务需求之间的鸿沟。
本章将逐一拆解 DDD 的五个核心构造块:实体、值对象、聚合、领域事件和限界上下文。理解这些概念是后续进行业务回放与领域建模的基石。
1.1 实体(Entity)
实体是具有唯一标识且生命周期持续变化的对象。两个实体即使所有属性值相同,只要标识不同,就是不同的对象。实体的核心特征是“身份”,而非属性。
- 唯一标识:通常使用 ID(UUID、自增主键、业务主键)来区分。
- 可变性:实体的属性可以随时间变化,但身份不变。
- 业务行为:实体封装了与其身份相关的业务逻辑。
Go 实践提示:在 Go 中,实体通常定义为 struct,并显式包含 ID 字段。避免使用指针作为标识比较的依据,应始终通过 ID 方法或字段判断相等性。
// 实体示例:用户
type User struct {
ID string // 唯一标识
Name string // 可变属性
Email string // 可变属性
CreatedAt time.Time
}
// 通过 ID 判断实体相等性
func (u *User) Equals(other *User) bool {
return u.ID == other.ID
}
1.2 值对象(Value Object)
值对象通过属性值来定义自身,没有唯一标识,且通常是不可变的。两个值对象如果所有属性都相同,则视为同一个对象。值对象用于描述实体的特征或业务中的度量概念。
- 无标识:只靠属性值区分。
- 不可变性:创建后属性不可修改,修改即创建新对象。
- 可替换性:可以自由替换为另一个属性相同的值对象。
- 自包含逻辑:值对象可以包含与其值相关的业务校验或计算。
| 对比维度 | 实体(Entity) | 值对象(Value Object) |
|---|---|---|
| 核心特征 | 唯一标识 | 属性值 |
| 可变性 | 可变 | 不可变 |
| 相等性判断 | 通过 ID | 通过所有属性 |
| 生命周期 | 独立存在,持续变化 | 依附于实体,可被替换 |
| 典型例子 | 用户、订单、商品 | 金额、地址、颜色、时间段 |
// 值对象示例:金额
type Money struct {
amount decimal.Decimal
currency string
}
// 不可变:返回新对象而非修改原对象
func (m Money) Add(other Money) (Money, error) {
if m.currency != other.currency {
return Money{}, errors.New("currency mismatch")
}
return Money{
amount: m.amount.Add(other.amount),
currency: m.currency,
}, nil
}
// 通过所有属性判断相等
func (m Money) Equals(other Money) bool {
return m.amount.Equal(other.amount) && m.currency == other.currency
}
1.3 聚合(Aggregate)
聚合是一组相关对象的一致性边界,它将实体和值对象组织成一个整体,对外只通过一个聚合根(Aggregate Root)进行访问。聚合保证了边界内的数据变更始终满足业务不变条件(Invariant)。
- 聚合根:聚合中唯一的实体,外部只能通过聚合根操作聚合内的对象。
- 一致性边界:聚合内的所有变更必须在一次事务中完成。
- 引用规则:聚合之间通过聚合根 ID 引用,而非直接持有对象引用。
聚合设计原则:
- 聚合要尽量小,只包含必须保持一致的属性。
- 聚合根负责维护不变条件。
- 跨聚合的修改应通过领域事件异步协调。
// 聚合根:订单
type Order struct {
ID string
customerID string // 引用其他聚合的 ID
items []OrderItem // 聚合内的实体
totalAmount Money // 聚合内的值对象
status OrderStatus
}
// 聚合内的实体:订单项
type OrderItem struct {
productID string
quantity int
price Money
}
// 聚合根方法:保证不变条件
func (o *Order) AddItem(productID string, qty int, price Money) error {
if o.status != OrderStatusPending {
return errors.New("only pending order can add items")
}
// 业务校验:数量必须为正
if qty <= 0 {
return errors.New("quantity must be positive")
}
o.items = append(o.items, OrderItem{
productID: productID,
quantity: qty,
price: price,
})
// 重新计算总金额
o.recalculateTotal()
return nil
}
1.4 领域事件(Domain Event)
领域事件表示领域中已经发生的、对业务有意义的事实。它用于解耦聚合之间的通信,将副作用从当前事务中分离出来,实现最终一致性。
- 过去时命名:如 OrderCreated、PaymentReceived。
- 不可变:事件一旦发布,内容不可更改。
- 携带数据:包含事件发生时相关的上下文信息。
- 异步处理:通常通过消息队列或事件总线分发。
// 领域事件定义
type OrderCreated struct {
OrderID string
CustomerID string
Total Money
CreatedAt time.Time
}
// 在聚合根方法中发布事件
func (o *Order) Confirm() ([]interface{}, error) {
if o.status != OrderStatusPending {
return nil, errors.New("order already confirmed")
}
o.status = OrderStatusConfirmed
// 返回领域事件,由基础设施层负责发布
return []interface{}{
OrderCreated{
OrderID: o.ID,
CustomerID: o.customerID,
Total: o.totalAmount,
CreatedAt: time.Now(),
},
}, nil
}
1.5 限界上下文(Bounded Context)
限界上下文是 DDD 中最宏观的边界概念。它将一个大型业务系统拆分为多个语义上自治的子领域,每个子领域拥有自己的通用语言、模型和实现。不同限界上下文之间的模型可以不同,甚至同名概念在不同上下文中含义也不同。
| 限界上下文 | 核心职责 | “用户”模型含义 | “订单”模型含义 |
|---|---|---|---|
| 订单上下文 | 订单创建、支付、发货 | 买家(含地址、信用等级) | 核心实体,包含商品明细、金额、状态 |
| 库存上下文 | 商品库存管理、出入库 | 仓库管理员 | 仅包含 SKU 和数量,无金额概念 |
| 营销上下文 | 优惠券、促销活动 | 目标用户群体(标签化) | 不直接处理订单,只关心订单金额范围 |
常见误区:不要试图为整个系统建立一个统一的“完美模型”。限界上下文允许甚至鼓励不同上下文对同一概念有不同的定义。跨上下文的模型映射通过防腐层(Anti-Corruption Layer)处理。
1.6 核心概念关系总览
下图展示了五个核心概念在 DDD 分层架构中的位置与协作关系。限界上下文是最大的边界,内部包含多个聚合;聚合由聚合根(实体)和内部的值对象、实体组成;聚合之间的状态变更通过领域事件进行异步通知。
1.7 本章小结
DDD 的五个核心概念构成了业务建模的完整工具链:
- 实体 负责有身份的业务主体,值对象 负责无身份的业务描述。
- 聚合 通过聚合根封装一致性边界,领域事件 实现跨聚合的异步解耦。
- 限界上下文 从宏观上划分业务领域,确保每个模型在其边界内自洽。
Go 工程落地要点:
- 实体和值对象使用 struct 定义,值对象建议不暴露指针修改方法。
- 聚合根方法返回事件切片,由应用层负责发布,保持领域层纯净。
- 限界上下文对应 Go 中的独立 package 或 module,通过接口依赖倒置。