Skip to content

设计原理 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);
        // ... 状态转换规则散落在服务方法里
    }
    // 几十个类似方法
}

问题

  1. 业务规则("任务完成要记录操作人"、"驳回要废弃其他任务")散落在服务层,换个服务类就换一套规则
  2. 领域对象对自身状态没有任何约束——谁都能 setState(DONE),规则无法内聚
  3. 代码膨胀:服务类越来越大,越来越难测

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 是聚合根?

  1. 事务边界:一个流程实例的所有任务变更应在同一事务内(一致性强约束)
  2. 不变量isAllTasksFinished() 决定 join 是否放行、实例是否完成——这些规则只能由聚合根统一判断
  3. 状态机归属:实例的 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普通 classdomain/ProcessInstance.java
Gostruct + receiver 方法model/instance.go
Node.jsclass(interface 承载不了行为)src/model.ts
Pythondataclass + 方法jeeflow/model.py

Node 特别说明:领域对象原来是 interface(纯数据),改为 class 后,内存仓储的 {...inst} 浅拷贝会丢失原型方法,因此仓储改用 cloneInstanceObject.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 节点

下一篇03 · 执行引擎——一次审批的完整旅程

jeeflow · 轻量级多语言工作流引擎