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上下文中属性不同
关键认知:限界上下文是微服务拆分的首要依据。错误的边界划分会导致“分布式单体”或过度通信。
限界上下文与微服务映射 订单上下文 通用语言:订单、商品、支付 OrderService (Go) OrderRepository OrderEventPublisher 数据库:order_db 库存上下文 通用语言:SKU、仓库、入库单 InventoryService (Go) StockRepository StockEventSubscriber 数据库:inventory_db 支付上下文 通用语言:交易、退款、对账单 PaymentService (Go) TransactionRepository PaymentGatewayAdapter 数据库:payment_db 事件 事件

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
}
领域事件流转与解耦 订单聚合 Order.Submit() → 产生 OrderSubmittedEvent 事件总线 RabbitMQ / Kafka 异步分发 库存聚合 扣减库存 通知聚合 发送确认邮件 物流聚合 创建发货单

2.7 六大概念关系总结

这六个概念并非孤立存在,它们共同构成了 DDD 的战术建模体系。以下表格展示了它们之间的协作关系:

概念 核心作用 与其它概念的关系 Go 实现要点
通用语言 统一沟通与代码 贯穿所有概念,是命名的基础 包名、类型名、方法名直接使用业务术语
限界上下文 划定模型边界 内部包含实体、值对象、聚合 每个上下文一个 Go module
实体 有标识的可变对象 可作为聚合根或聚合内部对象 struct + 显式 ID + 指针接收器方法
值对象 无标识的不可变对象 通常作为实体的属性 struct + 值接收器 + 工厂函数
聚合 保证数据一致性 由聚合根(实体)和内部对象组成 聚合根暴露方法,内部对象不对外暴露
领域事件 解耦聚合间通信 由聚合产生,被其他聚合消费 接口 + 具体事件 struct + 事件总线
实战心法:在 Go 微服务中,建议先通过事件风暴识别限界上下文和聚合边界,再定义实体和值对象,最后设计领域事件。代码结构应严格遵循通用语言,让业务专家也能读懂代码。