2. 报表领域模型:报表实体定义、报表值对象设计、报表聚合设计、报表仓储接口
领域模型是报表回放系统的核心骨架。在 DDD(领域驱动设计)中,报表领域模型通过实体、值对象、聚合与仓储四个层次,将业务规则与数据持久化解耦。本节将逐一拆解这四个关键构造块,并给出 Go 语言下的典型实现。
2.1 报表实体定义
实体(Entity)拥有唯一标识,且其状态会随时间或业务操作发生变化。在报表回放领域,报表实体(Report) 是聚合根,代表一次完整的报表生成任务。
| 字段 | 类型 | 说明 |
|---|---|---|
| ReportID | string (UUID) | 全局唯一标识,不可变 |
| TemplateID | string | 关联的报表模板标识 |
| Status | ReportStatus | 枚举:Pending / Running / Completed / Failed |
| CreatedAt | time.Time | 创建时间 |
| UpdatedAt | time.Time | 最后更新时间 |
// Report 是报表回放领域的聚合根实体
type Report struct {
ReportID string `json:"report_id"`
TemplateID string `json:"template_id"`
Status ReportStatus `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ReportStatus 表示报表生成状态
type ReportStatus string
const (
ReportPending ReportStatus = "pending"
ReportRunning ReportStatus = "running"
ReportCompleted ReportStatus = "completed"
ReportFailed ReportStatus = "failed"
)
设计要点:实体必须通过工厂方法创建,确保 ReportID 在构造函数中生成,避免外部篡改。状态变更应封装为领域方法(如
Start()、Complete()),而非直接暴露 Setter。
2.2 报表值对象设计
值对象(Value Object)没有唯一标识,通过属性值来定义自身。在报表领域,报表配置(ReportConfig) 和 数据源连接(DataSourceConnection) 是典型的值对象。
- 不可变性:值对象一旦创建,属性不可修改。需要变更时,应创建新实例。
- 相等性:两个值对象的所有属性相等时,视为同一个对象。
- 自包含:值对象不应引用实体或仓储。
// ReportConfig 是报表配置值对象
type ReportConfig struct {
OutputFormat string `json:"output_format"` // pdf, xlsx, html
Filters map[string]string `json:"filters"`
PageSize int `json:"page_size"`
}
// DataSourceConnection 是数据源连接值对象
type DataSourceConnection struct {
Host string `json:"host"`
Port int `json:"port"`
Database string `json:"database"`
Username string `json:"username"`
Password string `json:"-"` // 敏感字段,序列化时忽略
}
// Equals 比较两个值对象是否相等
func (c ReportConfig) Equals(other ReportConfig) bool {
return c.OutputFormat == other.OutputFormat &&
reflect.DeepEqual(c.Filters, other.Filters) &&
c.PageSize == other.PageSize
}
常见误区:不要为值对象定义 ID 字段,也不要使用指针接收器修改内部属性。值对象应通过值传递(非指针)在方法间传递,以保证不可变性。
2.3 报表聚合设计
聚合(Aggregate)是领域模型中的一致性边界。在报表回放场景中,Report 聚合 包含一个聚合根(Report 实体)和若干内部实体/值对象。聚合内部的所有变更必须通过聚合根对外暴露的方法完成,确保业务不变量始终成立。
// ReportAggregate 封装聚合操作
type ReportAggregate struct {
report *Report
config ReportConfig
tasks []*ReportTask
conn DataSourceConnection
}
// NewReportAggregate 创建新的报表聚合
func NewReportAggregate(templateID string, config ReportConfig, conn DataSourceConnection) *ReportAggregate {
return &ReportAggregate{
report: &Report{
ReportID: uuid.New().String(),
TemplateID: templateID,
Status: ReportPending,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
config: config,
tasks: make([]*ReportTask, 0),
conn: conn,
}
}
// Start 启动报表生成,仅当状态为 Pending 时允许
func (a *ReportAggregate) Start() error {
if a.report.Status != ReportPending {
return errors.New("only pending report can be started")
}
a.report.Status = ReportRunning
a.report.UpdatedAt = time.Now()
// 创建初始任务
a.tasks = append(a.tasks, NewReportTask(a.report.ReportID, "data_fetch"))
return nil
}
2.4 报表仓储接口
仓储(Repository)为聚合提供持久化与检索能力,使领域层不依赖具体数据库实现。仓储接口定义在领域层,实现放在基础设施层。
// ReportRepository 是报表聚合的仓储接口
type ReportRepository interface {
// Save 保存聚合(新增或更新)
Save(ctx context.Context, aggregate *ReportAggregate) error
// FindByID 根据聚合根 ID 查找完整聚合
FindByID(ctx context.Context, reportID string) (*ReportAggregate, error)
// FindByStatus 根据状态查找聚合列表
FindByStatus(ctx context.Context, status ReportStatus) ([]*ReportAggregate, error)
// Delete 删除聚合
Delete(ctx context.Context, reportID string) error
}
仓储设计原则:
- 仓储接口只操作聚合根,不直接操作内部实体或值对象。
- 方法命名使用业务语言(如
Save而非InsertOrUpdate)。 - 返回完整聚合,调用方不应再通过其他仓储补数据。
- 事务边界由应用服务控制,仓储内部不开启事务。
2.5 领域模型关系总览
下表总结了报表领域模型中各元素的职责与关系:
| 元素 | 类型 | 标识 | 可变性 | 持久化方式 |
|---|---|---|---|---|
| Report | 实体(聚合根) | ReportID | 可变 | 通过仓储整体持久化 |
| ReportTask | 实体(内部) | TaskID | 可变 | 随聚合一起持久化 |
| ReportConfig | 值对象 | 无 | 不可变 | 序列化为聚合的一部分 |
| DataSourceConnection | 值对象 | 无 | 不可变 | 序列化为聚合的一部分 |