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})
}
聚合与聚合根结构 聚合边界(事务一致性) 聚合根:Order ID: string(全局标识) OrderLine(实体) 本地标识 + 业务属性 Address(值对象) 无标识,不可变 外部只能持有聚合根引用,不能直接引用 OrderLine 或 Address

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 项目中,优先将业务逻辑放入实体或值对象的方法中。只有当逻辑涉及多个聚合或需要外部资源时,才使用领域服务。领域事件应作为聚合根方法的副产品发布,而非单独调用。