4. 报表导出领域建模(二):建立领域模型——聚合根(导出任务)、仓储接口、领域服务
在上一节中,我们完成了报表导出模块的限界上下文划分与通用语言定义。本节将基于这些成果,正式建立核心领域模型,包括:聚合根(ExportTask)、仓储接口(IExportTaskRepository)以及领域服务(IExportDomainService)。这三个元素构成了报表导出模块的领域层骨架,决定了业务逻辑的内聚性与可扩展性。
4.1 聚合根:ExportTask
ExportTask 是整个报表导出模块的聚合根。它封装了导出任务从创建、执行到完成(或失败)的完整生命周期,并保证内部状态变更的一致性。
4.1.1 聚合根设计原则
- 唯一标识:每个 ExportTask 通过
task_id全局唯一标识。 - 内部一致性:所有对导出任务状态的修改必须通过聚合根的方法进行,不允许外部直接修改内部实体或值对象。
- 边界明确:聚合根内部包含 ExportRequest(值对象)、ExportResult(值对象)、TaskStatus(枚举)等,外部只能通过聚合根引用这些对象。
4.1.2 核心属性与状态机
| 属性 | 类型 | 说明 |
|---|---|---|
task_id |
TaskId (值对象) | 全局唯一标识,由 UUID 生成 |
request |
ExportRequest (值对象) | 包含报表类型、筛选条件、导出格式等 |
status |
TaskStatus (枚举) | PENDING → PROCESSING → COMPLETED / FAILED |
result |
ExportResult (值对象) | 包含文件路径、文件大小、行数、耗时等 |
created_at |
datetime | 任务创建时间 |
updated_at |
datetime | 最后状态变更时间 |
4.1.3 聚合根行为方法
// Python 伪代码:ExportTask 聚合根
class ExportTask:
def __init__(self, task_id: TaskId, request: ExportRequest):
self.task_id = task_id
self.request = request
self.status = TaskStatus.PENDING
self.result = None
self.created_at = datetime.utcnow()
self.updated_at = self.created_at
def start_processing(self) -> None:
if self.status != TaskStatus.PENDING:
raise DomainException("只能从 PENDING 状态开始处理")
self.status = TaskStatus.PROCESSING
self.updated_at = datetime.utcnow()
def complete(self, result: ExportResult) -> None:
if self.status != TaskStatus.PROCESSING:
raise DomainException("只能从 PROCESSING 状态完成")
self.status = TaskStatus.COMPLETED
self.result = result
self.updated_at = datetime.utcnow()
def fail(self, error_message: str) -> None:
if self.status not in (TaskStatus.PENDING, TaskStatus.PROCESSING):
raise DomainException("只能从 PENDING 或 PROCESSING 状态失败")
self.status = TaskStatus.FAILED
self.result = ExportResult.failed(error_message)
self.updated_at = datetime.utcnow()
设计要点:聚合根的方法必须保证不变量。例如,不允许从 COMPLETED 状态再次调用 complete(),也不允许跳过 PROCESSING 直接完成。状态转换的合法性由聚合根内部校验。
4.2 仓储接口:IExportTaskRepository
仓储是领域层与基础设施层之间的抽象边界。它负责聚合根的持久化与重建,对外提供类似集合的访问接口。仓储接口定义在领域层,实现放在基础设施层。
4.2.1 仓储接口定义
// Python 伪代码:仓储接口
from abc import ABC, abstractmethod
class IExportTaskRepository(ABC):
@abstractmethod
def save(self, task: ExportTask) -> None:
"""保存聚合根(新增或更新)"""
pass
@abstractmethod
def find_by_id(self, task_id: TaskId) -> Optional[ExportTask]:
"""根据 ID 查找聚合根"""
pass
@abstractmethod
def find_by_status(self, status: TaskStatus, limit: int = 10) -> List[ExportTask]:
"""根据状态查找任务列表"""
pass
@abstractmethod
def delete(self, task_id: TaskId) -> None:
"""删除聚合根"""
pass
4.2.2 仓储设计要点
- 面向聚合根:仓储只操作聚合根,不直接操作内部实体或值对象。
- 事务边界:一个仓储方法对应一个事务,保证聚合根内部一致性。
- 查询能力:除了按 ID 查询,还应提供按状态、按时间范围等查询方法,但避免过度暴露数据库细节。
常见误区:不要在仓储接口中暴露数据库特定的查询方法(如
find_by_sql、find_by_criteria_with_pagination)。仓储接口应保持纯领域语义,具体查询优化放在基础设施层实现。
4.3 领域服务:IExportDomainService
当某个业务逻辑无法自然地归属于某个聚合根或值对象时,应将其建模为领域服务。在报表导出模块中,导出任务编排与数据查询+文件生成的协调逻辑适合放在领域服务中。
4.3.1 领域服务接口定义
// Python 伪代码:领域服务接口
class IExportDomainService(ABC):
@abstractmethod
def create_export_task(self, request: ExportRequest) -> ExportTask:
"""创建并初始化导出任务"""
pass
@abstractmethod
def execute_export(self, task_id: TaskId) -> ExportResult:
"""执行导出流程:查询数据 → 生成文件 → 更新任务状态"""
pass
@abstractmethod
def cancel_export(self, task_id: TaskId) -> None:
"""取消正在执行的导出任务"""
pass
4.3.2 领域服务与聚合根的协作
领域服务通常依赖仓储接口和基础设施服务(如文件存储、消息队列)的抽象。它负责编排多个聚合根或外部服务的交互,但不包含具体的技术实现。
协作示例:
1. 通过仓储加载 ExportTask 聚合根。
2. 调用
3. 调用数据查询服务(抽象)获取报表数据。
4. 调用文件生成服务(抽象)生成 Excel/CSV 文件。
5. 调用
6. 通过仓储保存聚合根。
execute_export 方法内部流程:1. 通过仓储加载 ExportTask 聚合根。
2. 调用
task.start_processing() 更新状态。3. 调用数据查询服务(抽象)获取报表数据。
4. 调用文件生成服务(抽象)生成 Excel/CSV 文件。
5. 调用
task.complete(result) 更新状态。6. 通过仓储保存聚合根。
4.4 领域模型结构总览
下图展示了报表导出模块领域模型的核心结构,包括聚合根、值对象、仓储接口与领域服务之间的关系。
4.5 模型边界与职责总结
| 模型元素 | 职责 | 典型方法 | 依赖 |
|---|---|---|---|
| ExportTask (聚合根) | 封装导出任务生命周期与状态一致性 | start_processing(), complete(), fail() | ExportRequest, ExportResult |
| IExportTaskRepository (仓储接口) | 提供聚合根的持久化抽象 | save(), find_by_id(), find_by_status() | ExportTask |
| IExportDomainService (领域服务) | 编排跨聚合根或外部服务的业务逻辑 | create_export_task(), execute_export() | 仓储接口、基础设施抽象 |
实践建议:在编码实现时,先定义领域层接口(聚合根、仓储接口、领域服务接口),再在基础设施层实现仓储和具体服务。这样能保证领域层不依赖任何技术细节,便于后续替换数据库、文件存储或消息队列。