🔔 本文 6000+ 字深度原创,含完整代码示例和生产级落地方案。创作不易,如果对你有帮助,请点赞 👍 收藏 ⭐ 关注 🔥 三连支持,你的认可是我持续输出的最大动力!
本文是「设计模式实战解读」系列第十二篇。系列文章统一按照 定义 → 痛点场景 → 模式结构 → 核心实现 → 真实应用 → 常见变种 → 优缺点 → 避坑指南 → FAQ 的结构展开,每篇聚焦一个模式讲透。
一句话定义
状态模式(State):允许一个对象在其内部状态改变时改变它的行为,看起来就像改变了它的类。把每个状态的行为封装成独立的对象,用"状态切换"替代"满屏的 if-else"。
归属:行为型模式。
一、没有状态模式时的痛点
我们在 iPaaS 平台里有一个"流程实例",它的生命周期是这样的:
创建(CREATED) → 排队中(QUEUED) → 执行中(RUNNING) → 成功(SUCCESS)
→ 失败(FAILED) → 重试中(RETRYING) → 执行中(RUNNING)
→ 最终失败(TERMINATED)
→ 暂停(PAUSED) → 执行中(RUNNING)
七八个状态,每个操作(执行、暂停、取消、重试)对不同状态的行为都不一样。最原始的写法:
java
public class FlowInstance {
private String status; // CREATED / QUEUED / RUNNING / SUCCESS / FAILED / PAUSED ...
public void execute() {
if ("CREATED".equals(status)) {
// 初始化上下文,投递到执行队列
initContext();
sendToQueue();
status = "QUEUED";
} else if ("QUEUED".equals(status)) {
throw new BizException("流程正在排队中,请勿重复执行");
} else if ("RUNNING".equals(status)) {
throw new BizException("流程正在执行中");
} else if ("PAUSED".equals(status)) {
// 恢复执行
resumeContext();
sendToQueue();
status = "RUNNING";
} else if ("FAILED".equals(status)) {
// 重试
resetFailedNodes();
sendToQueue();
status = "RETRYING";
} else if ("SUCCESS".equals(status) || "TERMINATED".equals(status)) {
throw new BizException("流程已结束,无法执行");
} else {
throw new BizException("未知状态: " + status);
}
}
public void pause() {
if ("RUNNING".equals(status)) {
saveSnapshot();
status = "PAUSED";
} else if ("QUEUED".equals(status)) {
removeFromQueue();
status = "PAUSED";
} else {
throw new BizException("当前状态不允许暂停: " + status);
}
}
public void cancel() {
if ("CREATED".equals(status) || "QUEUED".equals(status)) {
status = "CANCELLED";
} else if ("RUNNING".equals(status)) {
killRunningTasks();
status = "CANCELLED";
} else if ("PAUSED".equals(status)) {
cleanSnapshot();
status = "CANCELLED";
} else {
throw new BizException("当前状态不允许取消: " + status);
}
}
// retry() / resume() / complete() ... 每个方法又是一坨 if-else
}
问题一目了然:
- if-else 地狱 ------每个操作都要判断所有状态,
execute()方法 7 个分支,pause()3 个分支,cancel()4 个分支...... - 新增状态要改所有方法 ------加一个"审核中"状态,
execute()、pause()、cancel()、retry()全要改 - 状态转换散落各处 ------"RUNNING → PAUSED"写在
pause()里,"PAUSED → RUNNING"写在execute()里,看完整流转要翻遍所有方法 - 违反开闭原则 ------改一个状态的行为,要动整个
FlowInstance类 - 测试困难------想测"暂停中的流程被取消",得先构造一个特定状态的完整对象
核心诉求:把每个状态的行为独立出来,状态之间切换清晰,新增状态不影响已有代码。
二、模式结构
┌──────────────────────────────────┐
│ Context(上下文) │
│ - state: State │ ← 持有当前状态
│ - instanceId: String │
│ + execute() │ ← 委托给 state.execute()
│ + pause() │ ← 委托给 state.pause()
│ + cancel() │ ← 委托给 state.cancel()
│ + setState(State) │ ← 状态切换入口
└───────────────┬──────────────────┘
│ state.execute()
↓
┌──────────────────────────────────────────────────────────┐
│ State(状态接口) │
│ + execute(context): void │
│ + pause(context): void │
│ + cancel(context): void │
└───────┬──────┬──────┬──────┬──────┬──────┬───────────────┘
│ │ │ │ │ │
↓ ↓ ↓ ↓ ↓ ↓
Created Queued Running Success Failed Paused ...
四个角色:
- Context(上下文):持有当前状态对象的引用,对外暴露操作方法,所有操作委托给当前状态
- State(状态接口):定义所有状态共有的操作契约
- ConcreteState(具体状态):每个状态一个类,实现该状态下的行为,并决定切换到哪个状态
- 状态转换 :由 ConcreteState 在操作完成后调用
context.setState()完成切换
核心思想:把"状态判断"从 Context 移到 ConcreteState 里------每个状态自己管自己的行为,不再需要 if-else。
三、核心实现
3.1 定义状态接口
java
/**
* 流程实例状态接口------每个操作对应一种行为
*/
public interface FlowInstanceState {
/** 执行/恢复执行 */
void execute(FlowInstanceContext context);
/** 暂停 */
void pause(FlowInstanceContext context);
/** 取消 */
void cancel(FlowInstanceContext context);
/** 完成(由引擎回调) */
void complete(FlowInstanceContext context);
/** 失败(由引擎回调) */
void fail(FlowInstanceContext context, Exception error);
/** 状态名称,用于日志和监控 */
String name();
}
3.2 实现各具体状态
java
/**
* 创建状态------流程刚创建,还没投递到执行队列
*/
public class CreatedState implements FlowInstanceState {
@Override
public void execute(FlowInstanceContext context) {
// 初始化执行上下文
context.getFlowEngine().initContext(context.getInstanceId());
// 投递到执行队列
context.getFlowEngine().sendToQueue(context.getInstanceId());
// 状态切换:CREATED → QUEUED
context.setState(new QueuedState());
}
@Override
public void pause(FlowInstanceContext context) {
throw new BizException("流程尚未执行,无法暂停");
}
@Override
public void cancel(FlowInstanceContext context) {
// 创建态直接取消,无需清理
context.setState(new CancelledState());
}
@Override
public void complete(FlowInstanceContext context) {
throw new BizException("流程尚未执行,无法完成");
}
@Override
public void fail(FlowInstanceContext context, Exception error) {
throw new BizException("流程尚未执行,无法标记失败");
}
@Override
public String name() { return "CREATED"; }
}
/**
* 执行中状态------节点正在逐个执行
*/
public class RunningState implements FlowInstanceState {
@Override
public void execute(FlowInstanceContext context) {
throw new BizException("流程正在执行中,请勿重复触发");
}
@Override
public void pause(FlowInstanceContext context) {
// 保存执行快照(用于后续恢复)
context.getFlowEngine().saveSnapshot(context.getInstanceId());
// 中断当前节点
context.getFlowEngine().interruptCurrentNode(context.getInstanceId());
// 状态切换:RUNNING → PAUSED
context.setState(new PausedState());
}
@Override
public void cancel(FlowInstanceContext context) {
// 杀死正在执行的任务
context.getFlowEngine().killRunningTasks(context.getInstanceId());
context.setState(new CancelledState());
}
@Override
public void complete(FlowInstanceContext context) {
context.getFlowEngine().finalizeInstance(context.getInstanceId());
context.setState(new SuccessState());
}
@Override
public void fail(FlowInstanceContext context, Exception error) {
context.getFlowEngine().recordError(context.getInstanceId(), error);
// 判断是否还有重试机会
if (context.getRetryCount() < context.getMaxRetry()) {
context.setState(new RetryingState());
} else {
context.setState(new TerminatedState());
}
}
@Override
public String name() { return "RUNNING"; }
}
/**
* 暂停状态------快照已保存,等待恢复
*/
public class PausedState implements FlowInstanceState {
@Override
public void execute(FlowInstanceContext context) {
// 从快照恢复上下文
context.getFlowEngine().resumeFromSnapshot(context.getInstanceId());
// 重新投递执行
context.getFlowEngine().sendToQueue(context.getInstanceId());
// 状态切换:PAUSED → RUNNING
context.setState(new RunningState());
}
@Override
public void pause(FlowInstanceContext context) {
throw new BizException("流程已处于暂停状态");
}
@Override
public void cancel(FlowInstanceContext context) {
context.getFlowEngine().cleanSnapshot(context.getInstanceId());
context.setState(new CancelledState());
}
@Override
public void complete(FlowInstanceContext context) {
throw new BizException("暂停中的流程无法完成");
}
@Override
public void fail(FlowInstanceContext context, Exception error) {
throw new BizException("暂停中的流程无法标记失败");
}
@Override
public String name() { return "PAUSED"; }
}
3.3 Context 类------极其简洁
java
/**
* 流程实例上下文------所有操作委托给当前状态
*/
public class FlowInstanceContext {
private FlowInstanceState state;
private final String instanceId;
private final FlowEngine flowEngine;
private int retryCount;
private int maxRetry;
public FlowInstanceContext(String instanceId, FlowEngine flowEngine) {
this.instanceId = instanceId;
this.flowEngine = flowEngine;
this.state = new CreatedState(); // 初始状态
this.retryCount = 0;
this.maxRetry = 3;
}
// 对外接口:全部委托给 state,Context 本身不含任何 if-else
public void execute() { state.execute(this); }
public void pause() { state.pause(this); }
public void cancel() { state.cancel(this); }
public void complete(){ state.complete(this); }
public void fail(Exception e) { state.fail(this, e); }
// 状态切换入口------由 ConcreteState 调用
public void setState(FlowInstanceState newState) {
FlowInstanceState oldState = this.state;
this.state = newState;
// 状态变更通知(日志/监控/持久化)
flowEngine.onStateChanged(instanceId, oldState.name(), newState.name());
}
public String getCurrentState() { return state.name(); }
public String getInstanceId() { return instanceId; }
public FlowEngine getFlowEngine() { return flowEngine; }
public int getRetryCount() { return retryCount; }
public void incrementRetryCount() { retryCount++; }
public int getMaxRetry() { return maxRetry; }
}
对比一下:重构前 Context 有七八个 if-else 分支,重构后 Context 只有委托调用------零 if-else。所有状态判断逻辑被分散到了各自的状态类里,每个状态类只管自己的事。
四、真实应用:iPaaS 连接器执行状态机
iPaaS 的连接器执行过程也有一个状态机。一个连接器调用从"发起请求"到"拿到结果",中间经历多个状态:
#mermaid-svg-0bt1wwYC9zunFUsW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0bt1wwYC9zunFUsW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0bt1wwYC9zunFUsW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0bt1wwYC9zunFUsW .error-icon{fill:#552222;}#mermaid-svg-0bt1wwYC9zunFUsW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0bt1wwYC9zunFUsW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0bt1wwYC9zunFUsW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0bt1wwYC9zunFUsW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0bt1wwYC9zunFUsW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0bt1wwYC9zunFUsW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0bt1wwYC9zunFUsW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0bt1wwYC9zunFUsW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0bt1wwYC9zunFUsW .marker.cross{stroke:#333333;}#mermaid-svg-0bt1wwYC9zunFUsW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0bt1wwYC9zunFUsW p{margin:0;}#mermaid-svg-0bt1wwYC9zunFUsW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0bt1wwYC9zunFUsW .cluster-label text{fill:#333;}#mermaid-svg-0bt1wwYC9zunFUsW .cluster-label span{color:#333;}#mermaid-svg-0bt1wwYC9zunFUsW .cluster-label span p{background-color:transparent;}#mermaid-svg-0bt1wwYC9zunFUsW .label text,#mermaid-svg-0bt1wwYC9zunFUsW span{fill:#333;color:#333;}#mermaid-svg-0bt1wwYC9zunFUsW .node rect,#mermaid-svg-0bt1wwYC9zunFUsW .node circle,#mermaid-svg-0bt1wwYC9zunFUsW .node ellipse,#mermaid-svg-0bt1wwYC9zunFUsW .node polygon,#mermaid-svg-0bt1wwYC9zunFUsW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0bt1wwYC9zunFUsW .rough-node .label text,#mermaid-svg-0bt1wwYC9zunFUsW .node .label text,#mermaid-svg-0bt1wwYC9zunFUsW .image-shape .label,#mermaid-svg-0bt1wwYC9zunFUsW .icon-shape .label{text-anchor:middle;}#mermaid-svg-0bt1wwYC9zunFUsW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0bt1wwYC9zunFUsW .rough-node .label,#mermaid-svg-0bt1wwYC9zunFUsW .node .label,#mermaid-svg-0bt1wwYC9zunFUsW .image-shape .label,#mermaid-svg-0bt1wwYC9zunFUsW .icon-shape .label{text-align:center;}#mermaid-svg-0bt1wwYC9zunFUsW .node.clickable{cursor:pointer;}#mermaid-svg-0bt1wwYC9zunFUsW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0bt1wwYC9zunFUsW .arrowheadPath{fill:#333333;}#mermaid-svg-0bt1wwYC9zunFUsW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0bt1wwYC9zunFUsW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0bt1wwYC9zunFUsW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0bt1wwYC9zunFUsW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0bt1wwYC9zunFUsW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0bt1wwYC9zunFUsW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0bt1wwYC9zunFUsW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0bt1wwYC9zunFUsW .cluster text{fill:#333;}#mermaid-svg-0bt1wwYC9zunFUsW .cluster span{color:#333;}#mermaid-svg-0bt1wwYC9zunFUsW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0bt1wwYC9zunFUsW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0bt1wwYC9zunFUsW rect.text{fill:none;stroke-width:0;}#mermaid-svg-0bt1wwYC9zunFUsW .icon-shape,#mermaid-svg-0bt1wwYC9zunFUsW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0bt1wwYC9zunFUsW .icon-shape p,#mermaid-svg-0bt1wwYC9zunFUsW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0bt1wwYC9zunFUsW .icon-shape .label rect,#mermaid-svg-0bt1wwYC9zunFUsW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0bt1wwYC9zunFUsW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0bt1wwYC9zunFUsW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0bt1wwYC9zunFUsW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 发起调用
连接成功
连接失败
响应成功
超时
响应异常
转换成功
转换异常
重试
重试
重试
空闲
连接中
请求中
连接失败
数据转换中
超时
请求失败
完成
转换失败
4.1 连接器执行状态接口
java
public interface ConnectorExecState {
void onEnter(ConnectorExecContext context);
void execute(ConnectorExecContext context);
void onTimeout(ConnectorExecContext context);
void onError(ConnectorExecContext context, Exception e);
String name();
}
4.2 关键状态实现
java
/**
* 请求中状态------HTTP请求已发出,等待响应
*/
public class RequestingState implements ConnectorExecState {
@Override
public void onEnter(ConnectorExecContext context) {
// 启动超时计时器
context.getTimerService().schedule(
context.getRequestId(),
context.getTimeout(),
() -> context.onTimeout() // 超时回调
);
// 发送HTTP请求
HttpResponse response = context.getHttpClient().execute(
context.getHttpRequest());
context.setHttpResponse(response);
// 响应成功:进入数据转换
context.setState(new TransformingState());
}
@Override
public void execute(ConnectorExecContext context) {
throw new BizException("请求正在执行中");
}
@Override
public void onTimeout(ConnectorExecContext context) {
context.getTimerService().cancel(context.getRequestId());
// 超时:判断是否重试
if (context.canRetry()) {
context.incrementRetry();
context.setState(new IdleState()); // 回到空闲,重新发起
} else {
context.setState(new TimedOutState());
}
}
@Override
public void onError(ConnectorExecContext context, Exception e) {
context.getTimerService().cancel(context.getRequestId());
if (context.canRetry()) {
context.incrementRetry();
context.setState(new IdleState());
} else {
context.setState(new FailedState(e));
}
}
@Override
public String name() { return "REQUESTING"; }
}
4.3 状态切换自动持久化
我们在 setState() 里埋了一个钩子:每次状态切换自动写数据库 + 发消息。这样即使引擎宕机重启,也能从最后一个持久化的状态恢复执行:
java
public class ConnectorExecContext {
public void setState(ConnectorExecState newState) {
ConnectorExecState oldState = this.state;
this.state = newState;
// 1. 持久化状态到数据库
stateRepository.updateState(requestId, newState.name());
// 2. 写入执行日志
logService.logStateChange(requestId, oldState.name(), newState.name());
// 3. 发布状态变更事件(监控/告警消费)
eventPublisher.publish(new StateChangedEvent(
requestId, oldState.name(), newState.name()));
// 4. 触发新状态的 onEnter
newState.onEnter(this);
}
}
状态模式 + 持久化的组合威力:状态机每走一步都落库,宕机后重启时加载最后的状态对象,从断点继续执行。这就是我们 iPaaS 流程引擎断点续跑的核心机制。
五、状态模式 vs 策略模式
这两个模式的结构几乎一模一样------都是一个 Context 持有一个接口引用,都有多个实现类。很多人分不清。
| 对比维度 | 状态模式(State) | 策略模式(Strategy) |
|---|---|---|
| 切换时机 | 运行时自动切换(状态流转) | 由调用方主动选择 |
| 状态之间 | 有流转关系,A→B→C | 彼此独立,无流转 |
| 谁决定切换 | 状态对象自己(context.setState()) |
Context 或调用方 |
| 核心目的 | 消除状态判断的 if-else | 消除算法选择的 if-else |
| 典型场景 | 订单/审批/流程实例状态机 | 排序算法/折扣策略/路由策略 |
一句话区分:策略模式是"我选一个工具来用",状态模式是"我变成另一个人了"。
举个例子:
java
// 策略模式:调用方选择用哪个折扣算法
context.setStrategy(new VipDiscountStrategy());
context.calculatePrice(order);
// 状态模式:状态自动流转,调用方不知道也不关心
context.execute(); // CREATED → QUEUED → RUNNING → SUCCESS,自动的
六、常见变种
6.1 枚举状态(轻量版)
当状态行为比较简单时,用枚举代替状态类,减少类的数量:
java
public enum SimpleFlowState {
CREATED {
@Override
public void execute(FlowInstanceContext ctx) {
ctx.getEngine().sendToQueue(ctx.getId());
ctx.setState(QUEUED);
}
@Override
public void cancel(FlowInstanceContext ctx) {
ctx.setState(CANCELLED);
}
},
QUEUED {
@Override
public void execute(FlowInstanceContext ctx) {
throw new BizException("排队中");
}
@Override
public void cancel(FlowInstanceContext ctx) {
ctx.getEngine().removeFromQueue(ctx.getId());
ctx.setState(CANCELLED);
}
},
CANCELLED {
@Override
public void execute(FlowInstanceContext ctx) {
throw new BizException("已取消");
}
@Override
public void cancel(FlowInstanceContext ctx) {
throw new BizException("已经是取消状态");
}
};
public abstract void execute(FlowInstanceContext ctx);
public abstract void cancel(FlowInstanceContext ctx);
}
适合状态数量少(≤5 个)、行为简单的场景。状态多了还是拆类更清晰。
6.2 状态表驱动(配置化)
把状态转换规则从代码搬到配置表,运行时加载:
源状态 | 事件 | 目标状态 | 动作
CREATED | execute | QUEUED | initContext + sendToQueue
QUEUED | dequeue | RUNNING | startExecution
RUNNING | complete | SUCCESS | finalizeInstance
RUNNING | fail | RETRYING | incrementRetry
RETRYING | execute | RUNNING | resumeExecution
PAUSED | execute | RUNNING | resumeFromSnapshot
java
// 状态表驱动的状态机
public class TableDrivenStateMachine {
private final Map<String, Map<String, Transition>> transitions;
public void fireEvent(String event) {
Transition t = transitions.get(currentState).get(event);
if (t == null) {
throw new BizException("状态 " + currentState + " 不支持事件 " + event);
}
t.getAction().execute(context);
currentState = t.getTargetState();
}
}
优势:新增状态只需加配置行,不改代码。我们的 iPaaS 流程引擎就是表驱动的------用户在界面上配置"失败后重试 3 次"、"超时后转人工",底层就是状态表的行。
6.3 层级状态(Hierarchical State)
当多个状态有共同行为时,用继承避免重复:
java
/**
* 活跃状态基类------RUNNING / RETRYING / PAUSED 都属于"活跃"
*/
public abstract class ActiveState implements FlowInstanceState {
@Override
public void cancel(FlowInstanceContext context) {
// 所有活跃状态都可以被取消
context.getFlowEngine().cleanup(context.getInstanceId());
context.setState(new CancelledState());
}
}
public class RunningState extends ActiveState { /* ... */ }
public class RetryingState extends ActiveState { /* ... */ }
public class PausedState extends ActiveState { /* ... */ }
RUNNING / RETRYING / PAUSED 都继承了 ActiveState 的 cancel() 实现,不需要每个类都写一遍。
七、状态转换的可视化
状态模式最大的好处之一:状态流转天然形成一张图 。把每个状态类的 setState() 调用提取出来,就能自动生成状态转换图。
#mermaid-svg-AoJje6vkAQgHFw6q{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-AoJje6vkAQgHFw6q .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-AoJje6vkAQgHFw6q .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-AoJje6vkAQgHFw6q .error-icon{fill:#552222;}#mermaid-svg-AoJje6vkAQgHFw6q .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-AoJje6vkAQgHFw6q .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-AoJje6vkAQgHFw6q .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-AoJje6vkAQgHFw6q .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-AoJje6vkAQgHFw6q .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-AoJje6vkAQgHFw6q .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-AoJje6vkAQgHFw6q .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-AoJje6vkAQgHFw6q .marker{fill:#333333;stroke:#333333;}#mermaid-svg-AoJje6vkAQgHFw6q .marker.cross{stroke:#333333;}#mermaid-svg-AoJje6vkAQgHFw6q svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-AoJje6vkAQgHFw6q p{margin:0;}#mermaid-svg-AoJje6vkAQgHFw6q .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-AoJje6vkAQgHFw6q .cluster-label text{fill:#333;}#mermaid-svg-AoJje6vkAQgHFw6q .cluster-label span{color:#333;}#mermaid-svg-AoJje6vkAQgHFw6q .cluster-label span p{background-color:transparent;}#mermaid-svg-AoJje6vkAQgHFw6q .label text,#mermaid-svg-AoJje6vkAQgHFw6q span{fill:#333;color:#333;}#mermaid-svg-AoJje6vkAQgHFw6q .node rect,#mermaid-svg-AoJje6vkAQgHFw6q .node circle,#mermaid-svg-AoJje6vkAQgHFw6q .node ellipse,#mermaid-svg-AoJje6vkAQgHFw6q .node polygon,#mermaid-svg-AoJje6vkAQgHFw6q .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-AoJje6vkAQgHFw6q .rough-node .label text,#mermaid-svg-AoJje6vkAQgHFw6q .node .label text,#mermaid-svg-AoJje6vkAQgHFw6q .image-shape .label,#mermaid-svg-AoJje6vkAQgHFw6q .icon-shape .label{text-anchor:middle;}#mermaid-svg-AoJje6vkAQgHFw6q .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-AoJje6vkAQgHFw6q .rough-node .label,#mermaid-svg-AoJje6vkAQgHFw6q .node .label,#mermaid-svg-AoJje6vkAQgHFw6q .image-shape .label,#mermaid-svg-AoJje6vkAQgHFw6q .icon-shape .label{text-align:center;}#mermaid-svg-AoJje6vkAQgHFw6q .node.clickable{cursor:pointer;}#mermaid-svg-AoJje6vkAQgHFw6q .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-AoJje6vkAQgHFw6q .arrowheadPath{fill:#333333;}#mermaid-svg-AoJje6vkAQgHFw6q .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-AoJje6vkAQgHFw6q .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-AoJje6vkAQgHFw6q .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AoJje6vkAQgHFw6q .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-AoJje6vkAQgHFw6q .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AoJje6vkAQgHFw6q .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-AoJje6vkAQgHFw6q .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-AoJje6vkAQgHFw6q .cluster text{fill:#333;}#mermaid-svg-AoJje6vkAQgHFw6q .cluster span{color:#333;}#mermaid-svg-AoJje6vkAQgHFw6q div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-AoJje6vkAQgHFw6q .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-AoJje6vkAQgHFw6q rect.text{fill:none;stroke-width:0;}#mermaid-svg-AoJje6vkAQgHFw6q .icon-shape,#mermaid-svg-AoJje6vkAQgHFw6q .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-AoJje6vkAQgHFw6q .icon-shape p,#mermaid-svg-AoJje6vkAQgHFw6q .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-AoJje6vkAQgHFw6q .icon-shape .label rect,#mermaid-svg-AoJje6vkAQgHFw6q .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-AoJje6vkAQgHFw6q .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-AoJje6vkAQgHFw6q .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-AoJje6vkAQgHFw6q :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} execute
dequeue
complete
fail+可重试
fail+无重试
pause
execute
execute
cancel
cancel
cancel
cancel
Created
Queued
Running
Success
Retrying
Terminated
Paused
Cancelled
建议在项目里维护一份这样的图------新人入职看一眼就懂了,比自己翻代码快十倍。
八、优缺点
优点
- 消除 if-else------每个状态一个类,行为内聚,不再有大段条件判断
- 符合开闭原则------新增状态只需加一个类,不改已有代码
- 状态转换集中管理------每个状态类明确声明"我能切到哪些状态"
- 易于测试------测试某个状态的行为,只需构造对应的状态对象
- 状态可视化------状态转换天然可以画出状态图,文档自动生成
缺点
- 类数量增多------7 个状态就是 7 个类 + 1 个接口,小项目可能"杀鸡用牛刀"
- 状态间共享逻辑------如果多个状态有共同行为,需要引入基类或辅助类,否则代码重复
- 不适合简单场景------只有 2-3 个状态且行为简单时,if-else 反而更直观
九、避坑指南
坑 1:状态对象持有太多引用
症状:每个 ConcreteState 都要注入 10 个 Service,构造参数爆炸。
修复:把依赖集中到 Context 里,状态对象通过 Context 获取所需服务。状态类本身保持"瘦"------只写状态判断和流转逻辑。
java
// 错误:状态对象自己注入一堆依赖
public class RunningState implements FlowInstanceState {
@Autowired private FlowEngine engine; // ← 状态类不应该有 @Autowired
@Autowired private LogService logService;
@Autowired private MetricsService metrics;
}
// 正确:通过 Context 获取
public class RunningState implements FlowInstanceState {
@Override
public void complete(FlowInstanceContext context) {
context.getFlowEngine().finalizeInstance(context.getId()); // 通过 context 获取
context.setState(new SuccessState());
}
}
坑 2:忘记处理"非法转换"
症状:状态类里只写了"允许的操作",没写"不允许的操作"------调用方传了不该传的操作,直接 NPE 或静默忽略。
修复:接口里的每个方法,在每个状态类里都要有明确的实现------要么正常处理,要么抛出明确的异常。
java
// 错误:SuccessState 没实现 pause(),基类默认实现什么都不做
public class SuccessState implements FlowInstanceState {
// 缺少 pause() 实现 → 调用方以为暂停成功了
}
// 正确:明确拒绝
public class SuccessState implements FlowInstanceState {
@Override
public void pause(FlowInstanceContext context) {
throw new BizException("已完成的流程无法暂停");
}
}
坑 3:状态切换和业务操作顺序搞反
症状:先切状态再执行业务操作------如果业务操作失败了,状态已经变了,数据不一致。
修复 :先执行业务操作,再切换状态。如果业务操作抛异常,状态不会切换。
java
// 错误:先切状态
context.setState(new RunningState()); // ← 状态已经是 RUNNING 了
context.getEngine().startExecution(); // ← 如果这里失败了?状态不一致!
// 正确:先执行,再切状态
context.getEngine().startExecution(); // ← 失败直接抛异常
context.setState(new RunningState()); // ← 只有成功才切换
坑 4:状态对象有状态
症状:ConcreteState 类里有成员变量(比如计数器、时间戳),多个 Context 共享同一个状态对象时会串数据。
修复 :状态对象保持无状态------所有上下文信息都从 Context 参数里获取。如果确实需要状态类的单例共享,确保线程安全。
java
// 错误:状态对象有成员变量
public class RetryingState implements FlowInstanceState {
private int retryCount = 0; // ← 多个流程实例共享这个计数器!
}
// 正确:从 Context 获取
public class RetryingState implements FlowInstanceState {
@Override
public void execute(FlowInstanceContext context) {
int count = context.getRetryCount(); // ← 从 Context 获取,实例隔离
}
}
十、常见问题(FAQ)
Q:状态模式和 if-else 什么时候用哪个?
A:经验法则------状态 ≤ 3 个且每个状态只有 1-2 个操作,用 if-else 就够了。超过 3 个状态、或每个状态有 3 个以上操作、或未来可能新增状态,就上状态模式。状态模式的"前期成本"会在后续维护中连本带利收回。
Q:状态对象需要是单例吗?
A:看情况。如果状态对象无状态(推荐做法),可以做成单例,减少对象创建开销。用枚举天然就是单例。如果状态对象有差异化参数(比如 FailedState 需要携带错误信息),每次创建新实例。
Q:Spring StateMachine 框架怎么样?
A:Spring StateMachine 功能很全(支持层级状态、Guard/Action、持久化),但学习曲线陡、配置复杂。对于简单的状态机(5-8 个状态),手写状态模式更轻量、更可控。状态数量超过 15 个或需要复杂的 Guard 条件时,可以考虑引入框架。
Q:状态模式和工作流引擎有什么区别?
A:状态模式是代码层面 的设计模式,用 Java 类实现状态流转。工作流引擎(如 Camunda、我们的 iPaaS 流程引擎)是平台层面的工具,用可视化界面定义流程,底层可能用了状态模式。工作流引擎还额外提供了可视化编排、人工审批、超时处理等能力。
Q:如何在数据库中持久化状态?
A:推荐存状态的枚举字符串 (如 "RUNNING"),不要存数字。配合状态转换表做校验------每次更新前检查 旧状态 → 新状态 是否合法,防止并发场景下的状态跳跃。乐观锁(版本号)也是个好补充。
Q:并发场景下状态切换安全吗?
A:需要额外保障。常见方案:① 数据库行锁(UPDATE ... WHERE status = 'RUNNING');② 分布式锁(Redis/Zookeeper);③ 乐观锁(版本号)。状态模式本身不解决并发问题,并发是基础设施层的职责。
十一、小结
状态模式是解决"if-else 地狱"最直接的武器。三句话总结:
- 每个状态一个类------行为内聚、职责清晰,新增状态只加类不改旧代码
- 状态切换由状态对象自己决定------Context 只做委托,不参与状态判断
- 状态对象保持无状态------所有上下文信息从 Context 获取,避免多实例串数据
从 CREATED → QUEUED → RUNNING → SUCCESS,每一个状态转换都是代码里的一行 context.setState()------清晰、可追踪、可测试。当你发现自己在写第三个 if-else 判断同一个状态变量时,就是引入状态模式的最好时机。
预告 :下一篇我们聊聊备忘录模式------如何优雅地实现"撤销"和"断点恢复",让流程实例的快照管理不再是一坨序列化代码。
标签:#设计模式 #状态模式 #State #行为型模式 #状态机 #架构设计 #iPaaS #流程引擎 #if-else #代码质量 #设计模式实战 #技术分享 #Java #研发效能