2. 领域驱动设计(DDD)核心概念:实体、值对象、聚合、领域事件、仓库模式在Go中的实现

领域驱动设计(Domain-Driven Design,DDD)为复杂业务系统的建模提供了战术模式工具箱。在Go语言中实现DDD时,需要结合Go的简洁性与并发特性,将实体、值对象、聚合、领域事件和仓库模式转化为可执行的代码结构。本章聚焦于这些核心概念在Go中的落地方式,为后续的事件溯源与并发回放优化奠定领域模型基础。

2.1 实体(Entity)

实体是具有唯一标识且会随时间改变状态的对象。在Go中,实体通常表示为带有ID字段的结构体,并通过方法封装业务行为。

关键特征: 唯一标识(ID)、可变状态、业务方法封装。
// 用户实体
type User struct {
    ID        string    // 唯一标识
    Name      string
    Email     string
    CreatedAt time.Time
    UpdatedAt time.Time
}

// 业务方法:更新邮箱
func (u *User) UpdateEmail(email string) error {
    if !isValidEmail(email) {
        return errors.New("invalid email format")
    }
    u.Email = email
    u.UpdatedAt = time.Now()
    return nil
}

// 实体比较:基于ID而非字段值
func (u *User) Equals(other *User) bool {
    return u.ID == other.ID
}
特性 说明 Go实现要点
唯一标识 每个实体拥有不可变的ID 使用string或uuid类型,构造函数中生成
可变性 属性可通过方法修改 指针接收者方法修改字段
相等性 基于ID而非值比较 实现Equals方法或直接比较ID
生命周期 从创建到持久化再到删除 配合仓库模式管理

2.2 值对象(Value Object)

值对象通过属性值定义自身,没有唯一标识,且不可变。在Go中,值对象通常使用不可变结构体,通过构造函数创建,不暴露修改方法。

// 地址值对象
type Address struct {
    street   string // 小写字段,包外不可直接访问
    city     string
    zipCode  string
}

// 构造函数:确保值对象有效且不可变
func NewAddress(street, city, zipCode string) (Address, error) {
    if street == "" || city == "" || zipCode == "" {
        return Address{}, errors.New("all address fields are required")
    }
    return Address{street: street, city: city, zipCode: zipCode}, nil
}

// 值对象相等性:基于所有字段
func (a Address) Equals(other Address) bool {
    return a.street == other.street &&
           a.city == other.city &&
           a.zipCode == other.zipCode
}

// 只读访问器
func (a Address) Street() string  { return a.street }
func (a Address) City() string    { return a.city }
func (a Address) ZipCode() string { return a.zipCode }
Go实践建议: 值对象字段使用小写字母,通过访问器方法暴露只读视图。这符合Go的封装哲学,同时保证不可变性。

2.3 聚合(Aggregate)

聚合是领域驱动设计中保证数据一致性的边界。一个聚合包含一个根实体和一组相关的实体/值对象,所有外部访问必须通过聚合根进行。

聚合结构示例:订单聚合 聚合根:Order orderID, customerID, status, totalAmount 实体:OrderItem itemID, productID, quantity 值对象:Money amount, currency 值对象:ShippingAddress street, city, zipCode 实体:Payment paymentID, method, status 聚合边界 聚合规则 • 外部只能通过聚合根Order访问内部对象 • 一次事务只修改一个聚合实例 • 聚合内保证数据一致性,聚合间通过事件最终一致
// 订单聚合根
type Order struct {
    ID               string
    customerID       string
    items            []OrderItem
    shippingAddress  Address
    totalAmount      Money
    status           OrderStatus
    domainEvents     []DomainEvent // 待发布的领域事件
}

// 通过聚合根添加订单项
func (o *Order) AddItem(productID string, quantity int, unitPrice Money) error {
    if o.status != OrderStatusPending {
        return errors.New("can only add items to pending orders")
    }
    item := NewOrderItem(o.generateItemID(), productID, quantity, unitPrice)
    o.items = append(o.items, item)
    o.recalculateTotal()
    // 记录领域事件
    o.addDomainEvent(OrderItemAddedEvent{
        OrderID:   o.ID,
        ProductID: productID,
        Quantity:  quantity,
    })
    return nil
}

// 聚合内方法:重新计算总金额
func (o *Order) recalculateTotal() {
    var total float64
    for _, item := range o.items {
        total += item.SubTotal().Amount()
    }
    o.totalAmount = NewMoney(total, "USD")
}
并发注意: 在Go中,聚合实例可能被多个goroutine访问。建议在聚合根的方法中使用sync.Mutex或通过通道串行化访问,避免并发修改导致的不一致。

2.4 领域事件(Domain Event)

领域事件记录聚合中发生的业务相关事实,用于解耦聚合间的通信。在事件溯源架构中,领域事件是状态变更的唯一来源。

// 领域事件接口
type DomainEvent interface {
    EventID()   string
    OccurredAt() time.Time
    EventType() string
}

// 具体领域事件:订单已创建
type OrderCreatedEvent struct {
    EventIDVal    string    `json:"event_id"`
    OccurredAtVal time.Time `json:"occurred_at"`
    OrderID       string    `json:"order_id"`
    CustomerID    string    `json:"customer_id"`
    TotalAmount   float64   `json:"total_amount"`
}

func (e OrderCreatedEvent) EventID() string      { return e.EventIDVal }
func (e OrderCreatedEvent) OccurredAt() time.Time { return e.OccurredAtVal }
func (e OrderCreatedEvent) EventType() string     { return "order.created" }

// 领域事件发布器接口
type EventPublisher interface {
    Publish(event DomainEvent) error
}

// 聚合根中的事件收集与发布
func (o *Order) addDomainEvent(event DomainEvent) {
    o.domainEvents = append(o.domainEvents, event)
}

func (o *Order) ReleaseEvents() []DomainEvent {
    events := o.domainEvents
    o.domainEvents = nil
    return events
}
事件类型 触发场景 包含数据
OrderCreated 用户提交订单 orderID, customerID, totalAmount
OrderItemAdded 向订单添加商品 orderID, productID, quantity
OrderShipped 订单发货 orderID, trackingNumber, shippedAt
PaymentCompleted 支付成功 orderID, paymentID, amount

2.5 仓库模式(Repository Pattern)

仓库模式为聚合提供集合风格的访问接口,隐藏底层存储细节。在Go中,仓库通常定义为接口,支持依赖注入和测试替身。

仓库模式架构 应用服务 Application Service 仓库接口 OrderRepository interface 领域模型 Order Aggregate MySQL实现 MySQLOrderRepository 内存实现 InMemoryOrderRepository 事件溯源实现 EventStoreOrderRepository 仓库模式核心原则 • 接口定义在领域层,实现放在基础设施层 • 应用服务只依赖仓库接口,不依赖具体实现 • 每个聚合根对应一个仓库接口
// 仓库接口定义在领域层
type OrderRepository interface {
    Save(ctx context.Context, order *Order) error
    FindByID(ctx context.Context, orderID string) (*Order, error)
    FindByCustomer(ctx context.Context, customerID string, page, size int) ([]*Order, error)
    Delete(ctx context.Context, orderID string) error
}

// MySQL实现(基础设施层)
type MySQLOrderRepository struct {
    db *sql.DB
}

func (r *MySQLOrderRepository) Save(ctx context.Context, order *Order) error {
    tx, err := r.db.BeginTx(ctx, nil)
    if err != nil {
        return err
    }
    defer tx.Rollback()

    // 保存聚合根
    _, err = tx.ExecContext(ctx,
        `INSERT INTO orders (id, customer_id, status, total_amount, created_at, updated_at)
         VALUES (?, ?, ?, ?, ?, ?)
         ON DUPLICATE KEY UPDATE status=?, total_amount=?, updated_at=?`,
        order.ID, order.customerID, order.status, order.totalAmount.Amount(),
        order.CreatedAt, order.UpdatedAt,
        order.status, order.totalAmount.Amount(), order.UpdatedAt)
    if err != nil {
        return err
    }

    // 保存子实体
    for _, item := range order.items {
        _, err = tx.ExecContext(ctx,
            `INSERT INTO order_items (id, order_id, product_id, quantity, unit_price)
             VALUES (?, ?, ?, ?, ?)
             ON DUPLICATE KEY UPDATE quantity=?, unit_price=?`,
            item.ID, order.ID, item.ProductID, item.Quantity, item.UnitPrice.Amount(),
            item.Quantity, item.UnitPrice.Amount())
        if err != nil {
            return err
        }
    }

    return tx.Commit()
}

// 内存实现(测试用)
type InMemoryOrderRepository struct {
    mu     sync.RWMutex
    orders map[string]*Order
}

func (r *InMemoryOrderRepository) Save(ctx context.Context, order *Order) error {
    r.mu.Lock()
    defer r.mu.Unlock()
    r.orders[order.ID] = order
    return nil
}

func (r *InMemoryOrderRepository) FindByID(ctx context.Context, orderID string) (*Order, error) {
    r.mu.RLock()
    defer r.mu.RUnlock()
    order, ok := r.orders[orderID]
    if !ok {
        return nil, errors.New("order not found")
    }
    return order, nil
}

2.6 综合示例:订单创建流程

以下代码展示了实体、值对象、聚合、领域事件和仓库模式如何协同工作,完成一个完整的订单创建流程。

// 应用服务:订单服务
type OrderService struct {
    repo    OrderRepository
    publisher EventPublisher
}

func (s *OrderService) CreateOrder(ctx context.Context, customerID string, items []CreateItemRequest, shippingAddr Address) (*Order, error) {
    // 1. 创建聚合根
    order := NewOrder(generateID(), customerID, shippingAddr)

    // 2. 通过聚合根添加订单项
    for _, item := range items {
        unitPrice, _ := NewMoney(item.Price, "USD")
        if err := order.AddItem(item.ProductID, item.Quantity, unitPrice); err != nil {
            return nil, fmt.Errorf("failed to add item: %w", err)
        }
    }

    // 3. 保存聚合(仓库保证原子性)
    if err := s.repo.Save(ctx, order); err != nil {
        return nil, fmt.Errorf("failed to save order: %w", err)
    }

    // 4. 发布领域事件
    events := order.ReleaseEvents()
    for _, event := range events {
        if err := s.publisher.Publish(event); err != nil {
            // 事件发布失败的处理策略:重试、记录日志、或使用发件箱模式
            log.Printf("failed to publish event %s: %v", event.EventType(), err)
        }
    }

    return order, nil
}
Go DDD 最佳实践:
  • 实体和值对象放在 internal/domain 包中,不依赖任何外部框架
  • 仓库接口定义在领域层,实现放在 internal/infrastructure 包
  • 使用 wire 或 fx 进行依赖注入,解耦各层
  • 领域事件使用发件箱模式(Outbox Pattern)保证与聚合状态的一致性
  • 聚合内使用 sync.Mutex 保护并发访问,或通过通道串行化操作

2.7 本章小结

概念 Go实现要点 与事件溯源的关系
实体 指针接收者方法、ID比较 事件溯源中实体的状态由事件流重建
值对象 不可变结构体、小写字段、访问器 作为事件载荷的一部分被序列化
聚合 聚合根封装、事务边界、事件收集 事件溯源中聚合是事件流的投影
领域事件 接口定义、事件收集与发布 事件溯源的核心存储单元
仓库 接口在领域层、实现可替换 事件溯源仓库基于事件存储实现