1. 领域驱动设计基础:DDD 核心概念

领域驱动设计(Domain-Driven Design,DDD)是一种应对复杂业务系统的软件设计方法论,由 Eric Evans 在其经典著作《领域驱动设计》中系统提出。DDD 的核心思想是:软件系统的复杂性与业务领域的复杂性直接相关,因此设计应当以领域模型为中心,而非以数据表或技术框架为中心。

在《报表环境回放 Go 领域边界》课程中,DDD 是我们构建报表回放系统的理论基石。本节将系统梳理 DDD 的四大核心构造块:实体与值对象、聚合与聚合根、领域事件,并辅以 Go 语言的实现示例。

1.1 核心概念总览

概念 定义 核心特征 Go 实现要点
实体(Entity) 具有唯一标识、生命周期可追踪的对象 ID 决定身份,属性可变 struct + 唯一 ID 字段
值对象(Value Object) 由属性值定义、无唯一标识的不可变对象 值相等性,不可变,可替换 struct + 值比较方法
聚合(Aggregate) 一组相关对象的集群,保证数据一致性边界 事务边界,内部强一致 聚合根作为入口
聚合根(Aggregate Root) 聚合中唯一能被外部引用的实体 全局标识,外部只能通过它操作聚合 对外暴露的方法集合
领域事件(Domain Event) 领域中发生的、对业务有意义的过往事实 不可变,携带上下文,触发副作用 事件结构体 + 事件总线
关键认知:DDD 不是一套代码框架,而是一种建模思维。在 Go 中实现 DDD 时,我们更关注接口设计、包结构和领域逻辑的显式表达,而非依赖注入或 ORM 映射。

1.2 实体与值对象

实体与值对象是领域模型中最基本的两种对象类型,它们的区分直接决定了模型的质量。

实体(Entity)

实体拥有唯一标识,即使其他属性全部相同,只要 ID 不同就是不同的对象。实体的属性可以随时间变化,但身份保持不变。

Go 示例:报表回放系统中的“回放任务”实体

// TaskID 是回放任务的唯一标识
type TaskID string

// ReplayTask 是回放任务实体
type ReplayTask struct {
    ID        TaskID
    ReportID  string
    Status    TaskStatus
    CreatedAt time.Time
    UpdatedAt time.Time
}

// 实体比较:只比较 ID
func (t *ReplayTask) Equals(other *ReplayTask) bool {
    return t.ID == other.ID
}

值对象(Value Object)

值对象没有唯一标识,完全由属性值定义其含义。两个值对象如果所有属性值都相等,则视为同一个对象。值对象必须是不可变的——任何修改都应返回一个新实例。

Go 示例:报表查询条件值对象

// QueryCriteria 是报表查询条件值对象
type QueryCriteria struct {
    StartTime time.Time
    EndTime   time.Time
    Filters   map[string]string
}

// 值对象比较:比较所有字段
func (q QueryCriteria) Equals(other QueryCriteria) bool {
    if !q.StartTime.Equal(other.StartTime) {
        return false
    }
    if !q.EndTime.Equal(other.EndTime) {
        return false
    }
    if len(q.Filters) != len(other.Filters) {
        return false
    }
    for k, v := range q.Filters {
        if other.Filters[k] != v {
            return false
        }
    }
    return true
}

// 不可变:返回新实例
func (q QueryCriteria) WithFilter(key, value string) QueryCriteria {
    newFilters := make(map[string]string, len(q.Filters)+1)
    for k, v := range q.Filters {
        newFilters[k] = v
    }
    newFilters[key] = value
    return QueryCriteria{
        StartTime: q.StartTime,
        EndTime:   q.EndTime,
        Filters:   newFilters,
    }
}
实践建议:在 Go 中,值对象通常使用值接收者方法,实体使用指针接收者方法。这不仅是语法习惯,更是对两种对象语义的显式表达。

1.3 聚合与聚合根

聚合是 DDD 中最重要的一致性边界。一个聚合是一组相关对象的集群,外部只能通过聚合根访问聚合内部的任何对象。聚合保证了内部数据变更的原子性和一致性。

聚合设计原则

  • 边界明确:聚合内部的对象可以互相引用,但聚合之间只能通过聚合根 ID 引用。
  • 事务范围:一次请求只修改一个聚合,跨聚合的修改通过领域事件异步协调。
  • 根实体唯一:聚合根是聚合中唯一拥有全局标识的实体。

Go 示例:报表回放聚合

// ReplayAggregate 是回放聚合根
type ReplayAggregate struct {
    task      *ReplayTask      // 聚合根实体
    snapshots []*Snapshot      // 聚合内部的值对象集合
    events    []DomainEvent    // 未提交的领域事件
}

// 外部只能通过聚合根的方法操作
func (a *ReplayAggregate) StartReplay() error {
    if err := a.task.CanStart(); err != nil {
        return err
    }
    a.task.Status = TaskStatusRunning
    a.events = append(a.events, &ReplayStartedEvent{
        TaskID:   a.task.ID,
        StartedAt: time.Now(),
    })
    return nil
}

// 获取未提交事件(用于持久化后发布)
func (a *ReplayAggregate) UncommittedEvents() []DomainEvent {
    return a.events
}
常见误区:不要将数据库的外键关系直接映射为聚合之间的引用。聚合之间应通过ID 引用而非对象引用,否则会破坏聚合边界,导致级联修改失控。

1.4 领域事件

领域事件是领域中发生的、对业务有意义的过往事实。它通常以“过去时”命名,例如 ReplayStarted、ReportGenerated。领域事件的核心作用是解耦聚合之间的副作用,实现最终一致性。

领域事件的特征

特征 说明 Go 实现方式
不可变性 事件一旦发生,数据不可修改 struct 所有字段为只读
携带上下文 包含事件发生时所需的业务数据 字段包含 ID、时间戳、关键属性
发布与订阅 事件发布者不关心谁消费 事件总线或消息队列
幂等消费 同一事件被多次消费结果一致 消费端做去重或幂等检查

Go 示例:领域事件定义与发布

// DomainEvent 是领域事件接口
type DomainEvent interface {
    EventName() string
    OccurredAt() time.Time
}

// ReplayStartedEvent 回放开始事件
type ReplayStartedEvent struct {
    TaskID    TaskID
    StartedAt time.Time
}

func (e *ReplayStartedEvent) EventName() string {
    return "replay.started"
}

func (e *ReplayStartedEvent) OccurredAt() time.Time {
    return e.StartedAt
}

// 事件总线(简化版)
type EventBus struct {
    handlers map[string][]func(event DomainEvent)
}

func (b *EventBus) Publish(event DomainEvent) {
    for _, handler := range b.handlers[event.EventName()] {
        handler(event) // 同步或异步执行
    }
}

1.5 核心概念关系图

下图展示了 DDD 核心构造块之间的结构关系,以及它们在报表回放系统中的映射位置。

DDD 核心构造块关系图 DDD 核心构造块关系图 聚合边界(事务一致性) 聚合根(Aggregate Root) ReplayAggregate 实体(Entity) ReplayTask 值对象(Value Object) QueryCriteria 包含 包含 领域事件(Domain Event) ReplayStartedEvent · ReportGeneratedEvent · SnapshotTakenEvent 解耦聚合之间的副作用,实现最终一致性 产生事件 外部系统 / 其他聚合 通过聚合根 ID 引用,通过领域事件协作 事件驱动

1.6 在报表回放系统中的映射

将上述 DDD 概念映射到报表回放系统,可以得到清晰的领域模型边界:

DDD 概念 报表回放系统映射 说明
实体 ReplayTask、Report、User 每个都有唯一 ID,状态随时间变化
值对象 QueryCriteria、ReplayConfig、TimeRange 不可变,完全由属性定义
聚合根 ReplayAggregate、ReportAggregate 事务边界,外部只能通过根操作
领域事件 ReplayStarted、ReplayCompleted、ReportGenerated 解耦回放流程与通知、日志等副作用