2. 领域驱动设计基础:实体、值对象、聚合、领域服务、仓储模式的核心概念与Go中的实现思路

领域驱动设计(DDD)为复杂业务系统提供了一套严谨的建模语言。在CQRS架构中,DDD的战术建模工具——实体、值对象、聚合、领域服务与仓储——是构建高内聚、低耦合领域模型的核心基石。本章将逐一剖析这些概念,并给出在Go语言中的典型实现模式。

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

实体是具有唯一标识且可变的对象。两个实体即便所有属性相同,只要标识不同,就是不同的对象。在Go中,实体通常是一个包含标识字段和业务行为的结构体。

核心特征: 标识(ID)决定身份,属性可变,生命周期持续跟踪。
// User 实体:通过 UserID 唯一标识
type User struct {
    ID        UserID    // 值对象作为标识
    Name      string
    Email     string
    CreatedAt time.Time
}

// ChangeEmail 是实体的业务行为
func (u *User) ChangeEmail(email string) error {
    if !isValidEmail(email) {
        return ErrInvalidEmail
    }
    u.Email = email
    return nil
}
特性 实体 值对象
标识 有唯一ID 无标识,由属性值定义
可变性 可变(属性可更新) 不可变(替换而非修改)
相等性 ID相同即相等 所有属性相同即相等
生命周期 持续跟踪变化 无生命周期,用完即弃

2.2 值对象(Value Object):描述性、不可变的领域概念

值对象描述事物的属性,没有概念上的标识。例如金额、地址、颜色。在Go中,值对象应设计为不可变,通过函数返回新实例来“修改”。

// Money 值对象:不可变,通过 NewMoney 构造
type Money struct {
    amount   decimal.Decimal
    currency string
}

func NewMoney(amount decimal.Decimal, currency string) (Money, error) {
    if amount.IsNegative() {
        return Money{}, ErrNegativeAmount
    }
    return Money{amount: amount, currency: currency}, nil
}

// Add 返回新的 Money 实例,不修改原对象
func (m Money) Add(other Money) (Money, error) {
    if m.currency != other.currency {
        return Money{}, ErrCurrencyMismatch
    }
    newAmount := m.amount.Add(other.amount)
    return Money{amount: newAmount, currency: m.currency}, nil
}
Go实现要点: 值对象字段使用小写首字母(包内私有),通过构造函数(NewXXX)保证不变量;方法接收者使用值接收者(非指针),强化不可变语义。
实体 vs 值对象 对比图 实体(Entity) • 有唯一标识(ID) • 可变:属性可更新 • 生命周期持续跟踪 • 例:User, Order, Product Go:结构体 + 指针接收者 func (u *User) ChangeEmail() 值对象(Value Object) • 无标识,由属性定义 • 不可变:替换而非修改 • 无生命周期,用完即弃 • 例:Money, Address, Email Go:结构体 + 值接收者 func (m Money) Add(other Money) Money

2.3 聚合(Aggregate):一致性边界与事务单元

聚合是一组相关实体和值对象的一致性边界。外部只能通过聚合根(Aggregate Root)访问内部对象。聚合保证边界内的所有变更在单个事务中完成,是CQRS中命令模型的核心单元。

常见误区: 聚合不宜过大。一个聚合应只包含必须保持强一致性的对象。跨聚合的最终一致性通过领域事件和CQRS的事件总线实现。
// Order 聚合根
type Order struct {
    ID        OrderID
    customerID CustomerID
    items     []OrderItem  // 聚合内部实体
    status    OrderStatus  // 值对象
    version   int          // 乐观锁版本
}

// AddItem 是聚合根的业务方法,保证不变量
func (o *Order) AddItem(productID ProductID, quantity int, price Money) error {
    if o.status != OrderStatusPending {
        return ErrOrderNotModifiable
    }
    o.items = append(o.items, OrderItem{
        ProductID: productID,
        Quantity:  quantity,
        Price:     price,
    })
    return nil
}

// OrderItem 是聚合内部的实体,外部不可直接引用
type OrderItem struct {
    ProductID ProductID
    Quantity  int
    Price     Money
}
聚合原则 说明 Go实现提示
聚合根唯一入口 外部只能通过聚合根操作内部对象 内部实体字段小写,包外不可见
一致性边界 一个事务只修改一个聚合 仓储接口以聚合根为单位
版本控制 乐观锁防止并发冲突 聚合根内维护 version 字段
事件溯源 聚合变更产生领域事件 聚合根持有事件列表(待发布)

2.4 领域服务(Domain Service):无状态的业务编排者

当某个业务操作不属于任何实体或值对象时,应将其建模为领域服务。领域服务是无状态的,它协调多个聚合或外部资源完成一个业务用例。

// TransferService 领域服务:处理跨账户转账
type TransferService struct {
    accountRepo AccountRepository  // 依赖仓储接口
    currencySvc CurrencyConverter  // 依赖其他领域服务
}

func (s *TransferService) Transfer(ctx context.Context, fromID, toID AccountID, amount Money) error {
    from := s.accountRepo.FindByID(ctx, fromID)
    to := s.accountRepo.FindByID(ctx, toID)
    if from == nil || to == nil {
        return ErrAccountNotFound
    }
    // 调用聚合根的业务方法
    if err := from.Withdraw(amount); err != nil {
        return err
    }
    if err := to.Deposit(amount); err != nil {
        // 回滚:领域服务负责补偿
        from.Deposit(amount) // 简化示例
        return err
    }
    // 持久化
    s.accountRepo.Save(ctx, from)
    s.accountRepo.Save(ctx, to)
    return nil
}
命名规范: 领域服务通常以动词或业务活动命名,如 TransferService、PricingService、InventoryReservationService。避免使用 Manager、Util 等贫血命名。

2.5 仓储模式(Repository):领域对象的持久化抽象

仓储为聚合提供集合风格的访问接口,隐藏底层存储细节。在Go中,仓储通常定义为接口,位于领域层;实现位于基础设施层。

// OrderRepository 仓储接口:定义在领域层
type OrderRepository interface {
    FindByID(ctx context.Context, id OrderID) (*Order, error)
    Save(ctx context.Context, order *Order) error
    Delete(ctx context.Context, id OrderID) error
    FindByCustomerID(ctx context.Context, customerID CustomerID) ([]*Order, error)
}

// OrderRepositoryImpl 仓储实现:位于基础设施层
type OrderRepositoryImpl struct {
    db *sql.DB
}

func (r *OrderRepositoryImpl) Save(ctx context.Context, order *Order) error {
    // 使用事务保证聚合内所有变更原子提交
    tx, _ := r.db.BeginTx(ctx, nil)
    defer tx.Rollback()
    // 保存聚合根
    // 保存内部实体
    // 发布领域事件
    return tx.Commit()
}
DDD 战术建模分层架构 领域层(Domain Layer) 实体 | 值对象 | 聚合根 | 领域服务 | 仓储接口 | 领域事件 应用层(Application Layer) 应用服务 | 命令/查询处理 | 事务协调 | DTO转换 基础设施层(Infrastructure Layer) 仓储实现 | ORM映射 | 消息队列 | 缓存 | 外部API适配 仓储接口在领域层定义,实现倒置到基础设施层

2.6 Go实现中的关键设计决策

  1. 接口隔离: 仓储接口按聚合拆分,每个聚合一个接口,避免臃肿的通用仓储。
  2. 依赖注入: 领域服务通过构造函数注入仓储接口,便于单元测试和替换实现。
  3. 错误处理: 定义领域错误类型(如 ErrInsufficientFunds),而非使用泛型错误。
  4. 包结构: 按业务模块分包,每个模块包含实体、值对象、聚合、仓储接口和领域服务。
// 推荐的包结构示例
// internal/
//   order/
//     entity.go          // Order 聚合根, OrderItem 实体
//     value_object.go    // OrderID, OrderStatus, Money
//     repository.go      // OrderRepository 接口
//     service.go         // OrderService 领域服务
//   account/
//     entity.go
//     repository.go
//     service.go
总结: 实体提供标识与可变行为,值对象保证不可变与描述准确性,聚合划定一致性边界,领域服务编排跨聚合逻辑,仓储抽象持久化。这五个战术模式共同构成了CQRS架构中命令侧(写模型)的领域建模基础。在Go中,通过接口、结构体、值接收者与指针接收者的合理运用,可以精准实现DDD的战术设计意图。