4. 实体(Entity)设计:唯一标识、生命周期、Go中实体的实现模式、实体与数据库解耦

在领域驱动设计中,实体(Entity) 是拥有唯一标识且在其生命周期内状态持续变化的对象。与值对象不同,实体的核心不是“它是什么”,而是“它是谁”。本章将深入探讨如何在 Go 语言中设计健壮的实体,确保业务逻辑的完整性与持久化层的解耦。

4.1 实体的核心三要素

一个合格的领域实体必须同时满足以下三个条件:

要素 说明 Go 实现要点
唯一标识 在系统中唯一且不可变,用于区分不同实体实例 使用值对象封装 ID 类型,避免原始类型(如 string)直接暴露
生命周期 实体从创建、变更到归档/删除的完整过程 通过工厂方法创建,仓储负责持久化,领域事件记录关键状态变更
业务行为 实体内部封装业务规则,通过方法改变自身状态 贫血模型反模式:避免 Getter/Setter 泛滥,将行为内聚到实体方法中
设计提示: 实体的唯一标识一旦创建就不应被修改。在 Go 中,建议将 ID 字段设为私有(小写),并通过构造函数或工厂方法在初始化时赋值。

4.2 唯一标识:从原始类型到值对象

很多 Go 项目直接使用 int64 或 string 作为实体 ID,这会导致类型混淆和业务语义丢失。正确的做法是将 ID 封装为值对象:

// 错误做法:原始类型暴露,容易传错参数
type Order struct {
    ID        int64
    UserID    int64
    ProductID int64
}

// 正确做法:使用值对象封装唯一标识
type OrderID struct {
    value string // 内部使用 UUID 或 Snowflake
}

func NewOrderID(value string) (OrderID, error) {
    if !isValidUUID(value) {
        return OrderID{}, errors.New("invalid order ID format")
    }
    return OrderID{value: value}, nil
}

func (id OrderID) String() string {
    return id.value
}

type Order struct {
    id        OrderID
    userID    UserID
    productID ProductID
    status    OrderStatus
    createdAt time.Time
}

func NewOrder(id OrderID, userID UserID, productID ProductID) (*Order, error) {
    if id.IsZero() {
        return nil, errors.New("order ID must not be zero")
    }
    return &Order{
        id:        id,
        userID:    userID,
        productID: productID,
        status:    OrderStatusPending,
        createdAt: time.Now(),
    }, nil
}
避坑指南: 不要将 ID 生成逻辑放在实体内部。ID 生成策略(UUID、Snowflake、数据库自增)属于基础设施层,应通过工厂或仓储注入。实体只负责接收已生成的合法 ID。

4.3 生命周期管理:状态机与领域事件

实体的生命周期通常表现为一个有限状态机。以订单实体为例,其状态转换必须遵循业务规则:

type OrderStatus string

const (
    OrderStatusPending    OrderStatus = "pending"
    OrderStatusConfirmed  OrderStatus = "confirmed"
    OrderStatusShipped    OrderStatus = "shipped"
    OrderStatusDelivered  OrderStatus = "delivered"
    OrderStatusCancelled  OrderStatus = "cancelled"
)

var validTransitions = map[OrderStatus][]OrderStatus{
    OrderStatusPending:   {OrderStatusConfirmed, OrderStatusCancelled},
    OrderStatusConfirmed: {OrderStatusShipped, OrderStatusCancelled},
    OrderStatusShipped:   {OrderStatusDelivered},
    OrderStatusDelivered: {},
    OrderStatusCancelled: {},
}

func (o *Order) Confirm() error {
    if !o.canTransitionTo(OrderStatusConfirmed) {
        return fmt.Errorf("cannot confirm order in status %s", o.status)
    }
    o.status = OrderStatusConfirmed
    o.events = append(o.events, OrderConfirmedEvent{
        OrderID: o.id,
        Time:    time.Now(),
    })
    return nil
}

func (o *Order) canTransitionTo(target OrderStatus) bool {
    for _, allowed := range validTransitions[o.status] {
        if allowed == target {
            return true
        }
    }
    return false
}

生命周期中的关键节点应通过领域事件向外通知,而非直接调用外部服务。这保持了实体的纯净性。

订单实体生命周期状态机 待确认 已确认 已发货 已送达 已取消 虚线箭头表示“取消”操作,仅允许从“待确认”和“已确认”状态发起

4.4 Go 中实体的实现模式

在 Go 中实现实体时,应遵循以下模式来保证封装性和可测试性:

4.4.1 私有字段 + 公开方法

实体内部状态对外部完全隐藏,所有变更必须通过公开方法进行:

type User struct {
    id        UserID
    name      string
    email     Email
    role      UserRole
    updatedAt time.Time
    events    []DomainEvent // 领域事件列表
}

func (u *User) ChangeEmail(newEmail Email) error {
    if u.role == RoleAdmin && newEmail.Domain() != "company.com" {
        return errors.New("admin users must use company email domain")
    }
    u.email = newEmail
    u.updatedAt = time.Now()
    u.events = append(u.events, EmailChangedEvent{
        UserID:    u.id,
        OldEmail:  u.email,
        NewEmail:  newEmail,
        ChangedAt: time.Now(),
    })
    return nil
}

func (u *User) PullEvents() []DomainEvent {
    events := u.events
    u.events = nil
    return events
}

4.4.2 工厂方法替代直接构造

使用 NewXxx 函数作为实体的唯一创建入口,确保不变量在创建时即被满足:

func NewUser(id UserID, name string, email Email, role UserRole) (*User, error) {
    if name == "" {
        return nil, errors.New("user name must not be empty")
    }
    if role == RoleAdmin && email.Domain() != "company.com" {
        return nil, errors.New("admin must use company email")
    }
    return &User{
        id:        id,
        name:      name,
        email:     email,
        role:      role,
        updatedAt: time.Now(),
    }, nil
}

4.5 实体与数据库解耦:仓储模式

实体不应知道任何关于数据库的细节。通过仓储接口在领域层定义持久化契约,具体实现在基础设施层完成:

// 领域层:定义仓储接口
type OrderRepository interface {
    Save(ctx context.Context, order *Order) error
    FindByID(ctx context.Context, id OrderID) (*Order, error)
    FindByUserID(ctx context.Context, userID UserID, page, size int) ([]*Order, int64, error)
    Delete(ctx context.Context, id OrderID) error
}

// 基础设施层:实现仓储接口
type PostgresOrderRepository struct {
    db *sql.DB
}

func (r *PostgresOrderRepository) 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 (...) VALUES (...) ON CONFLICT DO UPDATE`)
    if err != nil {
        return err
    }

    // 保存订单项
    for _, item := range order.Items() {
        _, err = tx.ExecContext(ctx, `INSERT INTO order_items (...) VALUES (...)`...)
        if err != nil {
            return err
        }
    }

    return tx.Commit()
}
关键原则: 仓储接口属于领域层,仓储实现属于基础设施层。领域层代码(实体、值对象、领域服务)不应 import 任何 ORM 或数据库驱动包。
实体与数据库解耦:依赖倒置 领域层(Domain) Order 实体 OrderRepository 接口定义(无数据库依赖) 实体不 import 任何 ORM 基础设施层(Infrastructure) PostgresOrderRepo sql.DB 实现仓储接口 依赖具体数据库驱动 实现 依赖 依赖

4.6 实体设计检查清单

  • 唯一标识:是否使用值对象封装 ID?ID 生成是否与实体解耦?
  • 封装性:所有字段是否私有?状态变更是否通过公开方法进行?
  • 不变量:构造函数是否校验所有业务规则?状态转换是否受状态机约束?
  • 领域事件:关键业务操作是否产生事件?事件是否在实体内部收集?
  • 持久化解耦:实体是否不依赖任何 ORM 或数据库类型?仓储接口是否定义在领域层?
  • 贫血模型检查:实体中是否有业务逻辑?还是只有 Getter/Setter?
常见反模式: 将 JSON 标签、GORM 注解或数据库字段映射直接写在实体结构体上。这会导致领域层与基础设施层耦合,违反 DDD 的分层原则。正确的做法是使用单独的 DTO 或持久化模型进行转换。