4. 边界拆分原则:单一职责、高内聚低耦合、关注点分离在 DAG 调度中的落地

边界拆分是架构设计的核心动作。在 DAG 任务调度服务中,错误的边界会导致调度引擎臃肿、依赖混乱、任务回溯困难。本章将三个经典设计原则——单一职责、高内聚低耦合、关注点分离——映射到 DAG 调度的具体模块与代码边界上,给出可落地的拆分方案。

4.1 单一职责原则:每个模块只做一件事

单一职责原则(SRP)要求一个模块只对一个变化原因负责。在 DAG 调度中,常见的职责混淆包括:任务定义与执行逻辑混在一起、调度策略与任务状态存储耦合、依赖解析与失败重试逻辑交织。

职责域 错误耦合示例 正确拆分
任务定义 在 Task 类中同时包含 SQL 执行和 HTTP 调用 TaskSpec(定义) + TaskExecutor(执行)
调度策略 DAGScheduler 直接读写数据库状态 Scheduler(策略) + StateStore(持久化)
依赖解析 DAG 解析器内嵌重试逻辑 DAGParser(解析) + RetryHandler(重试)
任务日志 执行器直接写日志文件 Executor(执行) + LogCollector(采集)
落地检查清单: 每个模块的 public 方法数量不超过 5 个;模块描述能用一句话说清;修改一个模块的原因不超过一个业务维度。

4.2 高内聚低耦合:模块内部紧密,模块之间松散

高内聚指模块内部元素(类、函数、数据)共同完成一个明确功能;低耦合指模块之间的依赖关系简单、可替换。在 DAG 调度中,我们通过接口隔离和事件驱动实现这一目标。

4.2.1 模块内聚度评估指标

  • 功能内聚:模块内所有操作服务于同一任务(如 DAG 拓扑排序模块只做排序)。
  • 通信内聚:模块内函数共享同一输入数据结构(如 TaskContext 贯穿执行链路)。
  • 顺序内聚:模块内操作按固定流程执行(如任务提交 → 资源分配 → 执行 → 回调)。

4.2.2 耦合度控制策略

耦合类型 DAG 调度中的表现 解耦手段
内容耦合 调度器直接修改执行器的内部状态 通过回调接口通信
公共耦合 多个模块共享全局 DAG 状态变量 引入 DAGContext 作为显式上下文
控制耦合 上层模块传递 flag 控制下层行为 使用策略模式替代 if-else
数据耦合 模块间只传递基本类型参数 定义 TaskEvent、DAGEvent 等消息体
实践建议: 在 DAG 调度框架中,优先使用 事件总线 或 消息队列 解耦调度器与执行器。调度器只发布 TaskScheduled 事件,执行器订阅后自行处理,双方不直接调用。

4.3 关注点分离:横切关注点与核心逻辑的剥离

关注点分离(Separation of Concerns)将业务逻辑与横切关注点(日志、监控、鉴权、事务)分开。在 DAG 调度中,常见的横切关注点包括:

  • 任务执行监控:执行耗时、失败率、资源使用量。
  • 调度审计日志:谁在何时修改了 DAG 定义。
  • 权限校验:用户是否有权提交/终止任务。
  • 分布式事务:任务状态变更的一致性保证。

落地方式:使用 AOP(面向切面编程) 或 装饰器模式 将横切关注点织入核心流程,而不污染核心类。

// 伪代码:使用装饰器分离监控关注点
public class MonitoredTaskExecutor implements TaskExecutor {
    private final TaskExecutor delegate;
    private final MetricsCollector metrics;

    public MonitoredTaskExecutor(TaskExecutor delegate, MetricsCollector metrics) {
        this.delegate = delegate;
        this.metrics = metrics;
    }

    @Override
    public TaskResult execute(TaskContext ctx) {
        long start = System.currentTimeMillis();
        try {
            TaskResult result = delegate.execute(ctx);
            metrics.recordSuccess(ctx.taskId(), System.currentTimeMillis() - start);
            return result;
        } catch (Exception e) {
            metrics.recordFailure(ctx.taskId(), e);
            throw e;
        }
    }
}
常见陷阱: 不要将横切关注点直接写在 TaskExecutor 的实现中。一旦监控逻辑变更,所有执行器实现都需要修改。使用装饰器或 AOP 后,核心执行器只关心“如何执行任务”,监控是附加层。

4.4 边界拆分后的 DAG 调度架构图

下图展示了按照单一职责、高内聚低耦合、关注点分离原则拆分后的 DAG 调度服务模块边界。每个方框代表一个独立模块,箭头表示依赖方向(从调用方指向被调用方)。

边界拆分后的 DAG 调度模块架构 API 网关 Web 控制台 CLI/SDK DAG 定义管理 (解析/校验/版本控制) 拓扑排序引擎 (依赖解析/并行度计算) 调度策略执行器 (触发/重试/超时控制) 任务执行器 (SQL/Shell/HTTP 执行) 资源管理器 (线程池/连接池/配额) 状态持久化 (任务/DAG/日志存储) 监控 & 审计(AOP 装饰层) 权限 & 鉴权(AOP 装饰层) 说明:箭头表示调用/依赖方向。横切关注点通过 AOP 或装饰器织入,不直接修改核心模块代码。

4.5 边界拆分的代码组织示例

以下是一个按边界拆分后的包结构示例,每个包对应一个独立职责域:

com.company.dag/
├── api/                    # 用户接口层(单一职责:协议转换)
│   ├── DAGController.java
│   └── TaskController.java
├── core/                   # 调度核心层(高内聚:只做调度决策)
│   ├── dag/
│   │   ├── DAGDefinition.java
│   │   ├── DAGParser.java
│   │   └── TopologicalSorter.java
│   ├── schedule/
│   │   ├── Scheduler.java
│   │   ├── RetryPolicy.java
│   │   └── TimeoutManager.java
│   └── context/
│       └── DAGContext.java
├── executor/               # 执行层(关注点分离:只负责执行)
│   ├── TaskExecutor.java
│   ├── SqlExecutor.java
│   └── ShellExecutor.java
├── store/                  # 持久化层(单一职责:数据存取)
│   ├── DAGRepository.java
│   └── TaskStateStore.java
├── monitor/                # 横切关注点:监控(AOP)
│   └── ExecutionMonitorAspect.java
└── security/               # 横切关注点:鉴权(AOP)
    └── AuthorizationAspect.java
关键设计决策: 每个包只依赖 core/context 包中的 DAGContext,不直接依赖其他包的内部实现。这样当 executor 或 store 的实现变更时,core 层不受影响。

4.6 边界拆分后的依赖管理收益

原则 拆分前问题 拆分后收益
单一职责 Task 类 2000+ 行,同时处理定义、执行、日志 每个类 < 200 行,修改一个功能不影响其他
高内聚低耦合 调度器直接调用执行器内部方法,无法替换 通过接口依赖,可轻松切换本地/远程执行器
关注点分离 监控代码散落在所有执行方法中,难以统一治理 监控逻辑集中在 AOP 切面,核心代码零侵入

边界拆分不是一次性的架构活动,而是随着业务复杂度增长持续演进的过程。每次新增功能时,先问自己:这个功能属于哪个职责域?它应该依赖哪些模块?它是否引入了新的横切关注点? 坚持这三个问题,DAG 调度服务的边界就能始终保持清晰。