设计模式与工作流引擎:状态模式管理流程状态、责任链处理任务、模板方法定义流程
工作流引擎,说白了就是一套自动化的流程调度系统。你提交一个请假申请,它自动走审批、抄送、归档——这背后就是状态流转和任务处理。我这些年做过几个工作流相关的项目,从简单的审批流到复杂的工单系统,发现设计模式在这里简直是天作之合。
今天咱们就聊聊三个核心模式:状态模式管流程状态、责任链模式处理任务、模板方法模式定义流程骨架。这三个模式配合起来,能写出非常优雅的工作流引擎。
为什么是这三个模式?
工作流引擎的核心问题有三个:
- 状态怎么变?——每个节点都有前置状态和后置状态,状态迁移逻辑容易写成一团乱麻。
- 任务谁处理?——审批、通知、日志、校验……一堆任务要按顺序执行,还可能动态增减。
- 流程骨架怎么定?——不同业务(请假、报销、采购)流程不同,但步骤结构相似。
状态模式解决第一个问题,责任链解决第二个,模板方法解决第三个。我习惯把它们叫做「工作流三剑客」。
核心思路:状态模式负责「当前在哪」和「下一步能去哪」;责任链负责「到了这一步要做什么」;模板方法负责「整个流程长什么样」。
状态模式:让状态迁移变得清晰
先看状态模式。工作流里的每个节点,本质上就是一个状态。比如请假流程:草稿 → 部门审批 → 人事审批 → 结束。每个状态都知道自己能转移到哪些状态。
我见过很多新手把状态迁移写成 if-else 大法:
// 反面教材
if (currentState == "草稿" && action == "提交") {
currentState = "部门审批";
} else if (currentState == "部门审批" && action == "通过") {
currentState = "人事审批";
} else if (...) { ... }
这种代码,加一个新状态就要改一大片。我曾经在一个项目里接手过这样的代码,光状态迁移逻辑就写了 300 行,看得我头皮发麻。
用状态模式重构后,每个状态自己管自己的迁移:
// 状态接口
public interface WorkflowState {
String getStateName();
WorkflowState next(String action);
List<String> getAvailableActions();
}
// 具体状态:草稿
public class DraftState implements WorkflowState {
@Override
public String getStateName() { return "草稿"; }
@Override
public WorkflowState next(String action) {
if ("提交".equals(action)) {
return new DeptApprovalState();
}
throw new IllegalStateException("草稿状态不支持操作: " + action);
}
@Override
public List<String> getAvailableActions() {
return Arrays.asList("提交", "删除");
}
}
// 具体状态:部门审批
public class DeptApprovalState implements WorkflowState {
@Override
public String getStateName() { return "部门审批"; }
@Override
public WorkflowState next(String action) {
if ("通过".equals(action)) {
return new HrApprovalState();
} else if ("驳回".equals(action)) {
return new DraftState();
}
throw new IllegalStateException("部门审批状态不支持操作: " + action);
}
@Override
public List<String> getAvailableActions() {
return Arrays.asList("通过", "驳回");
}
}
你看,每个状态只关心自己能做什么、能去哪。加一个新状态,比如「总经理审批」,只需要新增一个类,实现接口,然后在对应的状态里加上迁移逻辑就行。完全符合开闭原则。
我的习惯:状态对象里不要存业务数据。状态只负责「迁移规则」,数据放在工作流上下文里。这样状态对象可以复用,不会因为数据不同而创建多个实例。
责任链模式:任务处理流水线
状态迁移搞定了,接下来是「到了这个状态要做什么」。比如进入「部门审批」状态后,需要:记录日志 → 发送通知 → 执行审批逻辑 → 更新数据库。这些任务有顺序,还可能动态调整。
责任链模式正好干这个。每个处理器只做一件事,然后交给下一个:
// 处理器接口
public interface TaskHandler {
void handle(TaskContext context);
TaskHandler setNext(TaskHandler next);
}
// 抽象基类
public abstract class AbstractTaskHandler implements TaskHandler {
private TaskHandler next;
@Override
public void handle(TaskContext context) {
doHandle(context);
if (next != null) {
next.handle(context);
}
}
protected abstract void doHandle(TaskContext context);
@Override
public TaskHandler setNext(TaskHandler next) {
this.next = next;
return next;
}
}
// 具体处理器:日志记录
public class LogHandler extends AbstractTaskHandler {
@Override
protected void doHandle(TaskContext context) {
System.out.println("记录日志: 状态变更为 " + context.getCurrentState());
}
}
// 具体处理器:通知发送
public class NotificationHandler extends AbstractTaskHandler {
@Override
protected void doHandle(TaskContext context) {
System.out.println("发送通知给 " + context.getAssignee());
}
}
// 具体处理器:数据库更新
public class DatabaseHandler extends AbstractTaskHandler {
@Override
protected void doHandle(TaskContext context) {
System.out.println("更新数据库: 流程ID=" + context.getProcessId());
}
}
组装起来也很直观:
TaskHandler chain = new LogHandler();
chain.setNext(new NotificationHandler())
.setNext(new DatabaseHandler());
// 执行整条链
chain.handle(context);
想加一个「短信提醒」?写个 SmsHandler,插到链里就行。想调整顺序?改一下组装代码。我曾经在一个项目中,用责任链把 20 多个乱七八糟的任务整理得清清楚楚,每个处理器不到 50 行代码。
注意:责任链里的处理器不要共享可变状态。每个处理器应该只依赖 TaskContext 里的数据,不要互相传参。否则链条一长,数据流就乱了。
模板方法模式:定义流程骨架
现在状态迁移和任务处理都有了,但不同业务的流程结构不同。比如请假流程是「提交→审批→结束」,报销流程是「提交→财务审核→主管审批→结束」。虽然步骤不同,但骨架相似:创建 → 流转 → 结束。
模板方法模式把骨架定好,具体步骤留给子类实现:
public abstract class WorkflowTemplate {
// 模板方法:定义流程骨架
public final void execute(WorkflowContext context) {
// 1. 初始化
init(context);
// 2. 循环处理状态迁移
while (!isFinished(context)) {
// 获取当前状态
WorkflowState currentState = context.getCurrentState();
// 3. 执行当前状态的任务链
TaskHandler handler = createTaskChain(context);
handler.handle(context);
// 4. 执行状态迁移
String action = getNextAction(context);
WorkflowState nextState = currentState.next(action);
context.setCurrentState(nextState);
// 5. 状态变更后的回调
onStateChanged(context, currentState, nextState);
}
// 6. 流程结束
onFinished(context);
}
// 子类可重写的方法
protected void init(WorkflowContext context) {}
protected void onStateChanged(WorkflowContext context, WorkflowState oldState, WorkflowState newState) {}
protected void onFinished(WorkflowContext context) {}
// 子类必须实现的方法
protected abstract boolean isFinished(WorkflowContext context);
protected abstract TaskHandler createTaskChain(WorkflowContext context);
protected abstract String getNextAction(WorkflowContext context);
}
具体业务只需要继承这个模板:
public class LeaveWorkflow extends WorkflowTemplate {
@Override
protected boolean isFinished(WorkflowContext context) {
return context.getCurrentState() instanceof EndState;
}
@Override
protected TaskHandler createTaskChain(WorkflowContext context) {
TaskHandler chain = new LogHandler();
chain.setNext(new NotificationHandler())
.setNext(new DatabaseHandler());
return chain;
}
@Override
protected String getNextAction(WorkflowContext context) {
// 实际项目中从用户输入或策略中获取
return "通过";
}
@Override
protected void onFinished(WorkflowContext context) {
System.out.println("请假流程结束,归档处理");
}
}
模板方法把「不变的部分」锁死,把「变的部分」开放出去。我习惯把模板方法写成 final,防止子类不小心改乱了流程骨架。
三者如何配合?
这三个模式不是孤立的,它们在工作流引擎里各司其职:
- 模板方法是导演,定好「先做什么、再做什么」。
- 状态模式是剧本,告诉导演「当前在哪、下一步能去哪」。
- 责任链是演员,每个演员只演好自己的戏份。
我画了一张图,帮你理解它们的关系:
实际运行时,流程是这样的:
- 模板方法的
execute()启动流程。 - 获取当前状态(比如「草稿」),调用责任链处理该状态下的任务。
- 任务处理完后,状态模式根据动作(比如「提交」)迁移到下一个状态。
- 重复 2-3 步,直到状态为「结束」。
避坑指南
最后分享几个我踩过的坑:
- 状态对象不要持有业务数据。我曾经把审批人信息放在状态对象里,结果不同流程实例共用同一个状态对象,数据全乱了。状态对象应该是无状态的,数据放上下文。
- 责任链不要搞成循环。有一次我写了个责任链,处理器 A 调 B,B 调 C,C 又调 A……直接栈溢出。责任链必须是单向的,或者用
break机制终止。 - 模板方法不要写太长。模板方法里如果塞了太多逻辑,子类重写起来很痛苦。保持骨架简洁,复杂逻辑拆到单独的方法里。
- 状态迁移要考虑回退。比如「驳回」操作要从「部门审批」回到「草稿」。状态模式天然支持这种回退,只要在
next()里返回对应的状态就行。
我的建议:刚开始用这三个模式时,先画状态图。把流程的所有状态和迁移画清楚,再写代码。状态图就是你的设计文档,比任何注释都管用。
好了,今天的内容就到这里。这三个模式配合起来,工作流引擎的核心骨架就搭好了。剩下的就是填充业务逻辑,比如审批策略、超时处理、并行节点等。但骨架稳了,加功能就不慌。