2. 领域驱动设计核心概念
领域驱动设计(DDD)提供了一套严谨的战术建模工具,用于将复杂的业务逻辑映射到软件模型中。本章将逐一剖析六大核心概念:实体、值对象、聚合、聚合根、领域服务与领域事件,并阐明它们之间的协作关系。
1. 实体(Entity)
实体是具有唯一标识且会经历生命周期变化的对象。两个实体即便所有属性相同,只要标识不同,就是不同的对象。
- 标识稳定性:标识一旦创建,通常不可变(如用户ID、订单ID)。
- 可变性:实体的属性可以随时间变化(如订单状态从“待支付”变为“已支付”)。
- 相等性:通过标识判断相等,而非属性值。
Go 示例: 实体通常使用 struct 表示,标识字段单独定义。
type Order struct {
ID string // 唯一标识
CustomerID string
Status string
CreatedAt time.Time
}
func (o *Order) ChangeStatus(newStatus string) {
// 业务逻辑校验
o.Status = newStatus
}
2. 值对象(Value Object)
值对象通过属性值来定义自身,没有唯一标识,且通常是不可变的。两个值对象如果所有属性相同,则视为同一个对象。
- 无标识:只描述事物的某个特征。
- 不可变性:修改值对象应返回新实例,而非修改原实例。
- 可替换性:值对象可以自由替换,不影响实体。
典型场景: 地址(Address)、金额(Money)、坐标(Coordinate)。
type Money struct {
Amount float64
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 + other.Amount, Currency: m.Currency}, nil
}
3. 聚合(Aggregate)与聚合根(Aggregate Root)
聚合是一组相关对象的一致性边界,聚合根是聚合内唯一允许外部引用的对象。所有对聚合内部对象的访问都必须通过聚合根。
| 概念 | 职责 | 规则 |
|---|---|---|
| 聚合 | 保证边界内数据一致性 | 一次事务只修改一个聚合 |
| 聚合根 | 对外暴露操作入口 | 全局唯一标识,外部只能持有聚合根引用 |
常见错误: 直接修改聚合内部的实体或值对象,绕过聚合根。这会导致一致性被破坏。
// 聚合根:Order
type Order struct {
ID string
Items []OrderLine // 聚合内部实体
Address Address // 值对象
}
// 通过聚合根添加订单行
func (o *Order) AddItem(productID string, quantity int, price Money) {
line := OrderLine{
ProductID: productID,
Quantity: quantity,
Price: price,
}
o.Items = append(o.Items, line)
// 触发领域事件
o.addDomainEvent(OrderLineAdded{OrderID: o.ID, ProductID: productID})
}
4. 领域服务(Domain Service)
当某个业务操作不属于任何实体或值对象的自然职责时,应将其建模为领域服务。领域服务是无状态的,只包含业务逻辑。
- 无状态:不持有业务状态,只执行计算或协调。
- 业务命名:方法名应体现业务意图(如
CalculateShippingCost)。 - 依赖注入:通常通过接口注入仓储或基础设施。
// 领域服务:运费计算
type ShippingService struct {
taxProvider TaxCalculator
}
func (s *ShippingService) CalculateShippingCost(order *Order, destination Address) (Money, error) {
// 复杂的运费计算逻辑
baseCost := Money{Amount: 10.0, Currency: "USD"}
tax, err := s.taxProvider.CalculateTax(destination)
if err != nil {
return Money{}, err
}
return baseCost.Add(tax)
}
5. 领域事件(Domain Event)
领域事件表示领域中已经发生的、对业务有意义的事实。它用于解耦聚合之间的通信,是事件驱动架构的核心。
- 过去时命名:如
OrderSubmitted、PaymentReceived。 - 不可变:事件一旦发布,内容不可更改。
- 包含上下文:携带事件发生时必要的数据(如聚合ID、时间戳)。
关键作用: 领域事件使得聚合之间可以最终一致性通信,而非强一致性事务。
// 领域事件定义
type OrderSubmitted struct {
OrderID string
CustomerID string
Total Money
OccurredAt time.Time
}
// 在聚合根中收集并发布事件
func (o *Order) Submit() error {
if o.Status != "pending" {
return errors.New("order already submitted")
}
o.Status = "submitted"
o.addDomainEvent(OrderSubmitted{
OrderID: o.ID,
CustomerID: o.CustomerID,
Total: o.CalculateTotal(),
OccurredAt: time.Now(),
})
return nil
}
6. 六大概念关系总结
下图展示了实体、值对象、聚合、聚合根、领域服务与领域事件之间的协作关系:
7. 概念对比速查表
| 概念 | 有无标识 | 可变性 | 生命周期 | 相等性判断 |
|---|---|---|---|---|
| 实体 | 有(全局或本地) | 可变 | 有 | 标识相等 |
| 值对象 | 无 | 不可变 | 无 | 属性全等 |
| 聚合根 | 有(全局唯一) | 可变 | 有 | 标识相等 |
| 领域服务 | 无 | 无状态 | 无 | 不适用 |
| 领域事件 | 有(事件ID) | 不可变 | 一次性 | 事件ID相等 |
实践建议: 在 Go 项目中,优先将业务逻辑放入实体或值对象的方法中。只有当逻辑涉及多个聚合或需要外部资源时,才使用领域服务。领域事件应作为聚合根方法的副产品发布,而非单独调用。