2. 领域驱动设计基础:实体、值对象、聚合、领域服务、仓储模式在报表场景下的概念映射
领域驱动设计(DDD)为复杂业务系统提供了清晰的建模方法论。在报表模块中,DDD 的核心构造块——实体、值对象、聚合、领域服务与仓储——需要结合报表的“只读分析”与“配置管理”双重特性进行重新映射。本章将逐一拆解这些概念在报表边界内的具体落地形态。
2.1 报表场景下的实体(Entity)
实体拥有唯一标识,且其状态会随时间或业务操作发生变化。在报表模块中,典型的实体包括:
- 报表定义(ReportDefinition):每个报表模板拥有全局唯一的 reportId,其属性(如 SQL 语句、参数列表、缓存策略)可被运维人员修改。
- 调度任务(ScheduleTask):定时生成报表的任务,拥有 taskId,状态可在“启用/停用/执行中”之间流转。
- 数据源配置(DataSourceConfig):数据库连接信息,拥有 dsId,密码轮换或连接池调整会改变其状态。
边界提示: 报表模块中的实体通常集中在“配置管理”子域。纯查询的报表结果(如数据集)不应建模为实体,因为其生命周期由查询触发,不具备独立标识。
2.2 报表场景下的值对象(Value Object)
值对象通过属性值定义自身,无唯一标识,且不可变。在报表模块中,值对象大量用于描述查询条件与结果结构:
| 值对象 | 属性 | 不可变原因 |
|---|---|---|
| QueryParam | name, type, defaultValue, validationRule | 参数定义一旦创建,不应被修改;修改应生成新版本 |
| ColumnMeta | fieldName, displayName, dataType, aggregation | 列元数据由 SQL 解析生成,修改无意义 |
| FilterCondition | field, operator, value | 条件组合由前端传入,后端只读使用 |
| ReportResult | rows, total, page, size | 查询结果是一次性快照,不应被修改 |
// Go 示例:值对象 QueryParam 的不可变设计
type QueryParam struct {
Name string
Type string // "string", "int", "date"
DefaultValue string
ValidationRule string
}
// 通过构造函数保证完整创建,不提供 Setter
func NewQueryParam(name, typ, defaultVal, rule string) (QueryParam, error) {
if name == "" {
return QueryParam{}, errors.New("param name required")
}
return QueryParam{Name: name, Type: typ, DefaultValue: defaultVal, ValidationRule: rule}, nil
}
2.3 报表场景下的聚合(Aggregate)
聚合是数据修改的一致性边界。在报表模块中,报表定义聚合是最核心的聚合根:
聚合设计原则: 报表模块中,一个聚合内的所有对象应同时加载、同时持久化。避免将“报表定义”与“数据源”放在同一聚合——数据源是独立生命周期,应作为另一个聚合根。
2.4 报表场景下的领域服务(Domain Service)
当某个业务操作无法自然地归属于某个实体或值对象时,应使用领域服务。报表模块中典型的领域服务包括:
- ReportExecutionService:负责执行报表 SQL、应用参数、处理分页与缓存。该服务协调 ReportDefinition(聚合根)与 DataSource(外部聚合)之间的交互。
- ReportValidationService:校验 SQL 语法、参数类型匹配、列权限过滤。该服务不修改任何聚合状态,只返回校验结果。
- ScheduleTriggerService:根据调度策略触发报表生成,涉及时间计算与任务队列分发。
// Go 示例:领域服务 ReportExecutionService
type ReportExecutionService struct {
reportRepo ReportRepository
dsRepo DataSourceRepository
cache ReportCache
}
func (s *ReportExecutionService) Execute(ctx context.Context, reportID string, params map[string]string) (*ReportResult, error) {
// 1. 加载聚合根
def, err := s.reportRepo.FindByID(ctx, reportID)
if err != nil {
return nil, fmt.Errorf("load report def failed: %w", err)
}
// 2. 校验参数(值对象)
validatedParams, err := def.ValidateParams(params)
if err != nil {
return nil, err
}
// 3. 获取数据源(跨聚合)
ds, err := s.dsRepo.FindByID(ctx, def.DataSourceID)
if err != nil {
return nil, err
}
// 4. 执行查询(领域服务协调)
result, err := s.executeQuery(ctx, ds, def.SQL, validatedParams)
if err != nil {
return nil, err
}
// 5. 缓存结果
s.cache.Set(reportID, params, result)
return result, nil
}
2.5 报表场景下的仓储模式(Repository)
仓储为聚合提供持久化与重建的抽象接口。在报表模块中,仓储应面向聚合根设计,而非每个表对应一个仓储:
| 仓储接口 | 对应聚合根 | 典型方法 |
|---|---|---|
| ReportRepository | ReportDefinition | FindByID, Save, Delete, SearchByTags |
| ScheduleRepository | ScheduleTask | FindByID, FindDueTasks, Save, Cancel |
| DataSourceRepository | DataSourceConfig | FindByID, FindAll, Save, TestConnection |
常见误区: 不要为值对象(如 QueryParam)单独创建仓储。值对象应通过聚合根仓储一起持久化。例如,ReportRepository.Save 应同时保存 ReportDefinition 及其内部的 QueryParams 列表。
2.6 概念映射总结
关键原则: 报表模块的 DDD 建模应遵循“配置用实体、查询用值对象、修改用聚合、协调用服务、持久化用仓储”的映射策略。这能有效隔离报表的“读”与“写”两条路径,避免将查询逻辑污染到领域模型中。