4. 领域驱动设计(DDD)核心概念:实体、值对象、聚合、领域服务

领域驱动设计(Domain-Driven Design,DDD)提供了一套用于构建复杂业务软件的战术建模工具。在 Go 项目中,正确区分并实现这些核心概念,是划定领域边界、保持代码可维护性的关键。本章将深入剖析四个最基础的战术模式:实体(Entity)、值对象(Value Object)、聚合(Aggregate) 和 领域服务(Domain Service)。

4.1 实体(Entity):拥有唯一标识的领域对象

实体是具有唯一标识且其状态会随时间变化的对象。在 Go 中,实体的核心是 ID 字段,它用于区分两个属性完全相同的对象。

  • 核心特征:可变性(Mutable)、唯一标识(Identity)、生命周期管理。
  • Go 实现要点:通常使用结构体(struct)表示,ID 字段建议使用值对象(如 UserID 类型)而非基础类型,以增强类型安全。
实战案例:用户实体
// entity/user.go
package entity

import "time"

// UserID 用户ID值对象
type UserID string

// User 用户实体
type User struct {
    ID        UserID    `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

// ChangeEmail 修改邮箱(实体行为)
func (u *User) ChangeEmail(newEmail string) error {
    // 领域规则:邮箱格式校验、唯一性校验(通常由领域服务或仓储完成)
    u.Email = newEmail
    u.UpdatedAt = time.Now()
    return nil
}

4.2 值对象(Value Object):描述属性的不可变对象

值对象通过属性值来定义自身,没有唯一标识,且通常是不可变的。当两个值对象的所有属性都相等时,它们被视为同一个对象。

  • 核心特征:不可变性(Immutable)、无标识、自包含逻辑、可替换。
  • Go 实现要点:不暴露 Setter 方法;通过构造函数创建;实现 Equals 方法比较值。
设计提示:值对象是消除“基本类型偏执”(Primitive Obsession)的最佳武器。将 string 类型的邮箱、int 类型的金额封装为值对象,可以内聚校验逻辑,避免散落在各处。
// valueobject/email.go
package valueobject

import "strings"

// Email 邮箱值对象
type Email struct {
    address string
}

// NewEmail 构造函数,包含校验逻辑
func NewEmail(address string) (Email, error) {
    if !strings.Contains(address, "@") {
        return Email{}, ErrInvalidEmail
    }
    return Email{address: address}, nil
}

// Address 获取邮箱地址(只读)
func (e Email) Address() string {
    return e.address
}

// Equals 值比较
func (e Email) Equals(other Email) bool {
    return e.address == other.address
}

4.3 聚合(Aggregate):事务一致性的边界

聚合是一组相关对象的集合,它作为一个整体对外提供操作,并保证内部数据的一致性。聚合根(Aggregate Root)是聚合中唯一能被外部访问的实体。

概念 说明 Go 实现示例
聚合根 聚合的入口,负责维护内部不变条件 Order 结构体
内部实体 聚合内的其他实体,只能通过聚合根访问 OrderItem 结构体
内部值对象 聚合内的值对象,用于描述属性 Address、Money
不变条件 聚合内必须始终满足的业务规则 订单总金额必须等于所有商品金额之和
常见误区:不要为了“方便查询”而将不相关的实体强行塞入同一个聚合。聚合的粒度应该由事务边界决定,而不是由数据库表关系决定。过大的聚合会导致性能问题和并发冲突。
// aggregate/order.go
package aggregate

import "errors"

// Order 订单聚合根
type Order struct {
    ID          string
    CustomerID  string
    Items       []OrderItem
    TotalAmount Money
}

// OrderItem 订单项(聚合内部实体)
type OrderItem struct {
    ProductID string
    Quantity  int
    UnitPrice Money
}

// AddItem 添加商品(保证不变条件)
func (o *Order) AddItem(productID string, qty int, price Money) error {
    if qty <= 0 {
        return errors.New("quantity must be positive")
    }
    item := OrderItem{
        ProductID: productID,
        Quantity:  qty,
        UnitPrice: price,
    }
    o.Items = append(o.Items, item)
    o.recalculateTotal()
    return nil
}

// recalculateTotal 重新计算总金额(内部方法)
func (o *Order) recalculateTotal() {
    total := Money{Amount: 0}
    for _, item := range o.Items {
        total = total.Add(item.UnitPrice.Multiply(item.Quantity))
    }
    o.TotalAmount = total
}
聚合边界与内部结构 聚合边界(事务一致性) 聚合根:Order ID, CustomerID, TotalAmount 内部实体:OrderItem ProductID, Quantity, UnitPrice 值对象:Money Amount, Currency 外部只能通过 Order 访问内部对象

4.4 领域服务(Domain Service):无状态的操作编排

当某个领域操作无法自然地归属于某个实体或值对象时,就需要引入领域服务。领域服务是无状态的,它负责编排多个领域对象来完成一个业务用例。

  • 适用场景:涉及多个聚合的协作、依赖外部资源(如仓储)、复杂的计算规则。
  • 命名规范:通常以动词或动词短语命名,如 TransferService、OrderPlacementService。
  • 与应用服务的区别:领域服务包含业务逻辑和规则;应用服务负责事务控制、权限校验等非业务逻辑。
实战案例:转账领域服务
// domain/service/transfer_service.go
package service

import (
    "context"
    "errors"
    "your-project/domain/aggregate"
    "your-project/domain/repository"
)

// TransferService 转账领域服务
type TransferService struct {
    accountRepo repository.AccountRepository
}

func NewTransferService(repo repository.AccountRepository) *TransferService {
    return &TransferService{accountRepo: repo}
}

// Transfer 执行转账操作(涉及两个聚合的协作)
func (s *TransferService) Transfer(ctx context.Context, fromID, toID string, amount aggregate.Money) error {
    from, err := s.accountRepo.FindByID(ctx, fromID)
    if err != nil {
        return err
    }
    to, err := s.accountRepo.FindByID(ctx, toID)
    if err != nil {
        return err
    }

    // 领域规则:扣款
    if err := from.Withdraw(amount); err != nil {
        return err
    }
    // 领域规则:入账
    to.Deposit(amount)

    // 仓储持久化(事务由应用层控制)
    if err := s.accountRepo.Save(ctx, from); err != nil {
        return err
    }
    if err := s.accountRepo.Save(ctx, to); err != nil {
        return err
    }
    return nil
}
领域服务协作流程 TransferService (领域服务) Account(from) Withdraw(amount) Account(to) Deposit(amount) AccountRepository Save() 应用层调用入口 Transfer(ctx, from, to, amount)

4.5 概念对比与选择指南

维度 实体(Entity) 值对象(Value Object) 聚合(Aggregate) 领域服务(Domain Service)
标识 有唯一 ID 无标识,靠值相等 聚合根有 ID 无状态,无标识
可变性 可变 不可变 聚合根可变 无状态
生命周期 有 无 有(由聚合根管理) 无
业务逻辑 自身行为 自包含校验 内部不变条件 跨聚合编排
Go 实现 struct + ID struct + 构造函数 struct + 聚合根 struct + 方法
决策速查:
- 需要区分两个对象?→ 实体
- 只关心属性值,不关心是谁的?→ 值对象
- 需要保证一组对象的事务一致性?→ 聚合
- 操作涉及多个聚合,或依赖外部资源?→ 领域服务