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)保证不变量;方法接收者使用值接收者(非指针),强化不可变语义。
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()
}
2.6 Go实现中的关键设计决策
- 接口隔离: 仓储接口按聚合拆分,每个聚合一个接口,避免臃肿的通用仓储。
- 依赖注入: 领域服务通过构造函数注入仓储接口,便于单元测试和替换实现。
- 错误处理: 定义领域错误类型(如
ErrInsufficientFunds),而非使用泛型错误。 - 包结构: 按业务模块分包,每个模块包含实体、值对象、聚合、仓储接口和领域服务。
// 推荐的包结构示例
// 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的战术设计意图。