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 领域服务与聚合根的协作

领域服务通常依赖仓储接口和基础设施服务(如文件存储、消息队列)的抽象。它负责编排多个聚合根或外部服务的交互,但不包含具体的技术实现。

协作示例:execute_export 方法内部流程:
1. 通过仓储加载 ExportTask 聚合根。
2. 调用 task.start_processing() 更新状态。
3. 调用数据查询服务(抽象)获取报表数据。
4. 调用文件生成服务(抽象)生成 Excel/CSV 文件。
5. 调用 task.complete(result) 更新状态。
6. 通过仓储保存聚合根。

4.4 领域模型结构总览

下图展示了报表导出模块领域模型的核心结构,包括聚合根、值对象、仓储接口与领域服务之间的关系。

报表导出领域模型结构图 报表导出领域模型结构 聚合根:ExportTask - task_id: TaskId - request: ExportRequest - status: TaskStatus - result: ExportResult - created_at / updated_at + start_processing() + complete() / fail() 值对象:ExportRequest - report_type: str - filters: dict - format: ExportFormat - max_rows: int 值对象:ExportResult - file_path: str - file_size: int - row_count: int 仓储接口 IExportTaskRepository + save(task) + find_by_id(id) + find_by_status() + delete(id) 领域服务 IExportDomainService + create_export_task() + execute_export() + cancel_export() 图例说明 实线箭头:聚合根包含/引用值对象 虚线箭头:领域服务与仓储之间的依赖/调用关系

4.5 模型边界与职责总结

模型元素 职责 典型方法 依赖
ExportTask (聚合根) 封装导出任务生命周期与状态一致性 start_processing(), complete(), fail() ExportRequest, ExportResult
IExportTaskRepository (仓储接口) 提供聚合根的持久化抽象 save(), find_by_id(), find_by_status() ExportTask
IExportDomainService (领域服务) 编排跨聚合根或外部服务的业务逻辑 create_export_task(), execute_export() 仓储接口、基础设施抽象
实践建议:在编码实现时,先定义领域层接口(聚合根、仓储接口、领域服务接口),再在基础设施层实现仓储和具体服务。这样能保证领域层不依赖任何技术细节,便于后续替换数据库、文件存储或消息队列。