Appearance
设计原理 02 · 领域模型——为什么把逻辑收敛到聚合根
系列:jeeflow 工作流引擎设计原理
1. 问题的起点:贫血模型 vs 充血模型
先看一个常见的工作流引擎实现(贫血模型):
java
// ❌ 贫血模型:ProcessInstance 只是数据袋子
public class ProcessInstance {
private Integer state; // 状态
private List<ProcessTask> tasks;
// 只有 getter/setter
}
// ❌ 上帝服务类:所有业务规则都堆在这里
@Service
public class ProcessTaskServiceImpl {
public void finishProcessTask(Long taskId, String operator, Dict args) {
ProcessTask task = baseMapper.selectById(taskId);
task.setTaskState(FINISHED);
task.setOperator(operator);
task.setFinishTime(new Date());
baseMapper.updateById(task);
// ... 状态转换规则散落在服务方法里
}
// 几十个类似方法
}问题:
- 业务规则("任务完成要记录操作人"、"驳回要废弃其他任务")散落在服务层,换个服务类就换一套规则
- 领域对象对自身状态没有任何约束——谁都能
setState(DONE),规则无法内聚 - 代码膨胀:服务类越来越大,越来越难测
2. 设计:聚合根封装规则
jeeflow 的领域模型分三层:
ProcessInstance(聚合根)
├── 自身状态:state / variables / operator
├── 子实体集合:tasks
└── 行为:
├── completeTask(task, operator, vars) ← 完成任务(驱动子实体)
├── abandonAllDoing(now) ← 废弃所有进行中(驳回/跳转用)
├── finish() / reject() ← 实例状态转换
├── createTask(...) ← 子实体工厂
└── isAllTasksFinished() ← join 合并判断
ProcessTask(子实体)
├── 自身状态:taskState / actorIds / finishTime
└── 行为:
├── finish(operator, vars, now) ← 10→20,记录操作人
├── abandon(now) ← 10→99
├── isAllowed(operator) ← 参与者权限
└── isDoing() / isFinished() ← 状态判断为什么 ProcessInstance 是聚合根?
- 事务边界:一个流程实例的所有任务变更应在同一事务内(一致性强约束)
- 不变量:
isAllTasksFinished()决定 join 是否放行、实例是否完成——这些规则只能由聚合根统一判断 - 状态机归属:实例的 10→20→45 转换、任务的 10→20/99 转换,只允许通过聚合根方法发生
引擎与聚合根的分工
引擎(编排) 聚合根(规则)
───────────────────── ─────────────────────
加载解析流程定义 完成任务时状态怎么变
决定下一个节点是谁 谁有权处理这个任务
评估决策表达式 驳回时哪些任务要废弃
解析参与者名单 任务创建时的字段约定
发布事件/调用拦截器 实例是否所有任务完成判断标准:改动涉及"状态/字段规则"→ 聚合根;涉及"流程走向/外部 IO"→ 引擎。
3. 伪代码:完成任务的状态机
completeTask(task, operator, vars, now):
task.finish(operator, vars, now) # 子实体:10→20,记录操作人/时间/变量
instance.variables = vars # 聚合根:合并流程变量
instance.updateTime = now
task.finish(operator, vars, now):
state = DONE(20)
actorId = operator
finishTime = now
variables = vars四版实现完全一致,仅语法不同。
4. 为什么引擎不直接改 task.state?
试想如果引擎直接写 task.taskState = 20:
- 操作人、完成时间、变量合并这三件事必须在三个地方重复写
- 将来加规则(如"完成任务自动生成抄送")要改所有调用点
- 测试只能通过引擎全链路测,无法单测领域规则
聚合根把这三件事内聚成一个方法后:引擎调用一次,规则只维护一处,单测直接测 completeTask。
5. 四版实现对照
| 语言 | 实现形式 | 关键文件 |
|---|---|---|
| Java | 普通 class | domain/ProcessInstance.java |
| Go | struct + receiver 方法 | model/instance.go |
| Node.js | class(interface 承载不了行为) | src/model.ts |
| Python | dataclass + 方法 | jeeflow/model.py |
Node 特别说明:领域对象原来是 interface(纯数据),改为 class 后,内存仓储的
{...inst}浅拷贝会丢失原型方法,因此仓储改用cloneInstance(Object.create(prototype)+ 浅拷贝字段)保留 class 原型。见src/model.ts的 clone 函数。
6. 聚合根方法清单
| 方法 | 职责 | 引擎调用场景 |
|---|---|---|
create(...) | 工厂,state=10 | 启动流程 |
completeTask(...) | 完成任务 + 合并变量 | executeProcessTask |
abandonAllDoing() | 废弃进行中任务 | 驳回 / 跳转 |
finish() | 10→20 | 到达 end 节点 |
reject() | 10→45 | 驳回(jumpToEnd) |
createTask(...) | 子实体工厂 | 所有任务创建点 |
isAllTasksFinished() | join 判断 | 引擎 join 节点 |