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