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 调度服务模块边界。每个方框代表一个独立模块,箭头表示依赖方向(从调用方指向被调用方)。
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 调度服务的边界就能始终保持清晰。