"为什么我的代码里 if (status == X) 写得到处都是?"—— 这是状态模式登场的最常见前奏。订单状态、连接状态、UI 状态、游戏角色状态:只要一个对象在不同状态下有不同行为,if/switch 就开始堆积。状态模式给出的答案是:把每个状态做成一个对象,对象的行为随状态对象切换而改变。这篇文章把状态模式从"if 地狱"讲到状态机框架、Redux finite state machines,讲清楚它和策略、命令的边界。
问题:状态变化的 if 矩阵
看一个订单的状态相关方法:
class Order {
enum Status { CREATED, PAID, SHIPPED, DELIVERED, CANCELLED, REFUNDED }
Status status;
public void pay() {
if (status == Status.CREATED) { status = Status.PAID; /* 扣款 */ }
else if (status == Status.CANCELLED) throw new IllegalStateException("已取消的订单不能付款");
else if (status == Status.PAID) throw new IllegalStateException("已付过款");
else throw new IllegalStateException("当前状态不能付款:" + status);
}
public void ship() {
if (status == Status.PAID) { status = Status.SHIPPED; /* 通知物流 */ }
else if (status == Status.CREATED) throw new IllegalStateException("未付款不能发货");
else throw new IllegalStateException("当前状态不能发货:" + status);
}
public void cancel() {
if (status == Status.CREATED) status = Status.CANCELLED;
else if (status == Status.PAID) { status = Status.REFUNDED; /* 退款 */ }
else if (status == Status.SHIPPED) throw new IllegalStateException("已发货,需走退货流程");
else throw new IllegalStateException("不能取消:" + status);
}
public void receive() { ... }
public void refund() { ... }
}
问题:每个动作的方法都要枚举所有"当前状态 × 该动作"的组合。状态机变复杂时,这个 if/else 矩阵失控。加一个新状态(比如 "FROZEN")要去所有方法里加 if 处理。加一个新动作(比如 "freeze")又要在每个状态下决定它的行为。改一处不漏地改完几乎是不可能任务。
状态模式:每个状态一个类
// 1. 抽象状态:定义所有可能的动作
interface OrderState {
void pay(OrderContext ctx);
void ship(OrderContext ctx);
void cancel(OrderContext ctx);
void receive(OrderContext ctx);
}
// 2. Context:持有当前状态,把动作委托给状态对象
class OrderContext {
private OrderState state;
private final Order order; // 真实订单数据
public OrderContext(Order order) {
this.order = order;
this.state = new CreatedState();
}
public void setState(OrderState s) { this.state = s; }
public Order order() { return order; }
public void pay() { state.pay(this); }
public void ship() { state.ship(this); }
public void cancel() { state.cancel(this); }
public void receive() { state.receive(this); }
}
// 3. 具体状态:每个状态决定自己的动作行为
class CreatedState implements OrderState {
public void pay(OrderContext ctx) {
// 扣款逻辑
ctx.order().setPaidAt(new Date());
ctx.setState(new PaidState());
}
public void ship(OrderContext ctx) { throw new IllegalStateException("未付款不能发货"); }
public void cancel(OrderContext ctx) { ctx.setState(new CancelledState()); }
public void receive(OrderContext ctx) { throw new IllegalStateException("未发货不能签收"); }
}
class PaidState implements OrderState {
public void pay(OrderContext ctx) { throw new IllegalStateException("已付过款"); }
public void ship(OrderContext ctx) {
// 通知物流
ctx.order().setShippedAt(new Date());
ctx.setState(new ShippedState());
}
public void cancel(OrderContext ctx) {
// 退款
refundService.refund(ctx.order());
ctx.setState(new RefundedState());
}
public void receive(OrderContext ctx) { throw new IllegalStateException("未发货不能签收"); }
}
class ShippedState implements OrderState {
public void pay(OrderContext ctx) { throw new IllegalStateException("已发货"); }
public void ship(OrderContext ctx) { throw new IllegalStateException("已发货过"); }
public void cancel(OrderContext ctx) { throw new IllegalStateException("已发货,请走退货"); }
public void receive(OrderContext ctx) {
ctx.order().setDeliveredAt(new Date());
ctx.setState(new DeliveredState());
}
}
class DeliveredState implements OrderState { /* 所有动作都拒绝或触发退货流程 */ }
class CancelledState implements OrderState { /* 所有动作都拒绝 */ }
class RefundedState implements OrderState { /* 所有动作都拒绝 */ }
对比改造前后:
- 每个状态的逻辑集中在自己的类里 —— 想知道"已付款时哪些动作可以做"就看 PaidState。
- 状态转换显式 ——
ctx.setState(new PaidState())一眼看清流转。 - 加新状态 = 新写一个类,旧状态不动(部分情况下抽象状态接口也不变)。
状态模式 vs 策略模式
结构上几乎一模一样 —— 都是 Context 持有一个接口,运行时切换具体实现。区别只在意图:
- 策略:同一件事的不同算法(快排 vs 归并),由外部决定用哪个。
- 状态:对象内部状态机的不同状态,由状态自己决定下一个状态(或由 Context 决定)。
策略客户端主动选,状态对象自己跳。这是核心差别。
实战 1:TCP 连接状态机
TCP 的 11 种状态(LISTEN、SYN_SENT、ESTABLISHED、FIN_WAIT_1...)是状态机的教科书例子。简化版:
interface TcpState {
void open(TcpContext ctx);
void close(TcpContext ctx);
void ack(TcpContext ctx);
void send(TcpContext ctx, byte[] data);
}
class ClosedState implements TcpState {
public void open(TcpContext ctx) { ctx.setState(new ListenState()); }
public void close(TcpContext ctx) { /* 已关闭 */ }
public void ack(TcpContext ctx) { /* 忽略 */ }
public void send(TcpContext ctx, byte[] data) { throw new IllegalStateException("not connected"); }
}
class EstablishedState implements TcpState {
public void open(TcpContext ctx) { /* 已连接 */ }
public void close(TcpContext ctx) { ctx.sendFin(); ctx.setState(new FinWait1State()); }
public void ack(TcpContext ctx) { /* 处理 ACK */ }
public void send(TcpContext ctx, byte[] data) { ctx.sendData(data); }
}
// ...其他 9 个状态
Netty、libuv、Tornado 内部都用状态模式管理网络连接的状态。如果直接用 if/switch,N×M 的(状态 × 事件)矩阵很快失控。
实战 2:UI 加载状态
type LoadingState =
| { kind: 'idle' }
| { kind: 'loading' }
| { kind: 'success'; data: User[] }
| { kind: 'error'; message: string };
const [state, setState] = useState<LoadingState>({ kind: 'idle' });
async function fetch() {
setState({ kind: 'loading' });
try {
const data = await api.getUsers();
setState({ kind: 'success', data });
} catch (e) {
setState({ kind: 'error', message: e.message });
}
}
function render(state: LoadingState) {
switch (state.kind) {
case 'idle': return <button onClick={fetch}>加载</button>;
case 'loading': return <Spinner />;
case 'success': return <UserList users={state.data} />;
case 'error': return <ErrorBanner msg={state.message} retry={fetch} />;
}
}
用判别联合(discriminated union)表达状态,每个 case 各自携带需要的数据 —— 这是状态模式在 TypeScript 里的现代表达。TS 的穷尽性检查能帮你确保新加状态时不漏。
实战 3:游戏角色状态
interface CharacterState {
void attack(Character c);
void defend(Character c);
void move(Character c);
}
class IdleState implements CharacterState {
public void attack(Character c) { c.setState(new AttackingState()); c.playAnimation("attack"); }
public void defend(Character c) { c.setState(new DefendingState()); }
public void move(Character c) { c.setState(new MovingState()); }
}
class AttackingState implements CharacterState {
public void attack(Character c) { /* 已经在攻击,忽略 */ }
public void defend(Character c) { /* 攻击中不能防御,或可中断 */ }
public void move(Character c) { /* 攻击中不能移动 */ }
}
class StunnedState implements CharacterState {
// 眩晕中所有动作都被忽略
public void attack(Character c) {}
public void defend(Character c) {}
public void move(Character c) {}
}
游戏 AI、动作游戏、棋牌游戏,处处需要状态管理。状态模式让"角色在不同状态下接受不同输入"的逻辑非常清晰。Unity 的 Animator State Machine、Unreal 的 Behavior Tree 都是状态机思想。
状态机框架:用 DSL 表达状态转移
当状态多到 10+ 时,逐个写状态类也累。这时用状态机框架更轻松:
// XState (JavaScript)
const orderMachine = createMachine({
id: 'order',
initial: 'created',
states: {
created: {
on: {
PAY: 'paid',
CANCEL: 'cancelled',
},
},
paid: {
on: {
SHIP: 'shipped',
CANCEL: 'refunding',
},
},
shipped: {
on: {
RECEIVE: 'delivered',
},
},
refunding: {
invoke: { src: 'refundService', onDone: 'refunded' },
},
delivered: { type: 'final' },
cancelled: { type: 'final' },
refunded: { type: 'final' },
},
});
const service = interpret(orderMachine).start();
service.send('PAY');
service.send('SHIP');
XState、Spring StateMachine、Akka FSM 都让你用声明式方式描述状态机,框架负责维护状态、检查合法转移、调用回调。比手写每个状态类清晰得多。
层次状态机(Hierarchical State Machine)
有些状态有"父状态"。例如游戏里"战斗中"是父状态,下面有"攻击中""防御中""被打中"等子状态。"战斗中"通用的事件(如"角色死亡 → 进入死亡状态")在父状态处理一次,子状态不用重复:
states: {
fighting: {
on: {
DIE: 'dead', // 任何子状态收到 DIE 都跳到 dead
},
initial: 'attacking',
states: {
attacking: { on: { ANIMATION_END: 'idle' } },
defending: { on: { ANIMATION_END: 'idle' } },
idle: { on: { ATTACK_INPUT: 'attacking' } },
},
},
dead: { type: 'final' },
}
层次状态机大幅减少重复转移。Statecharts(David Harel 提出的形式化模型)是状态机研究的里程碑,XState 实现了大多数 Statecharts 特性。
状态持久化
状态机最终要持久化(订单状态存数据库)。两种方案:
1. 存当前状态枚举
// 数据库表
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
status VARCHAR(20), -- 'created'/'paid'/'shipped'/...
...
);
// 加载时根据 status 创建对应 State 对象
OrderState state = switch (row.getString("status")) {
case "created" -> new CreatedState();
case "paid" -> new PaidState();
case "shipped" -> new ShippedState();
default -> throw new IllegalStateException();
};
2. 事件日志 + 重放(Event Sourcing)
-- 不存 status,存所有事件
CREATE TABLE order_events (
order_id BIGINT,
event_type VARCHAR(20), -- 'CREATED'/'PAID'/'SHIPPED'/...
payload JSON,
at TIMESTAMP
);
-- 重建状态:重放所有事件
function rebuildState(orderId) {
const events = fetchEvents(orderId);
let state = { kind: 'idle' };
for (const e of events) state = reducer(state, e);
return state;
}
Event Sourcing 的好处:历史完全可追溯,可以"穿越"到任何过去时刻;调试一个 bug 时能精确重放当时的状态变化。代价:重放有性能开销(可以加 snapshot 优化)。
状态机的常见坑
坑 1:状态字段散落多处
主状态字段是 status,但还有 isPaid、isShipped、isCancelled 等冗余字段,容易不同步。状态字段应该唯一,所有其他派生信息从它推导。
坑 2:非法状态可达
没用状态模式时,代码可能允许:状态是 "已发货" 但 paidAt 是 null。状态模式 + 数据库约束 + 业务规则三者一起守护"无效状态不可表示"。
坑 3:状态转移不原子
并发场景下"读当前状态 → 判断 → 写新状态"不原子,两个线程都通过判断都写,bug 出现。修复:用数据库乐观锁(UPDATE ... WHERE status = ? 检查行影响数)或悲观锁(SELECT ... FOR UPDATE)。
坑 4:state 之间共享可变数据
多个 State 实例共享 Context,如果 State 也持有可变字段就乱了。规则:State 对象应该是不可变的(无字段或全 final);真正可变的数据(订单字段)在 Context 里。
识别状态模式场景
- 对象有明确的状态(可以列出枚举值)。
- 不同状态下相同动作行为不同。
- 状态转移有明确规则(从哪些状态能转到哪些)。
- 状态数量在3 个以上,且能预见会扩展。
写在最后
状态模式是把"过程式的 if 判断"升级成"面向对象的多态分发"的标准操作。它在订单/连接/UI/游戏这些"对象生命周期复杂"的领域里几乎不可或缺。能不能写出"加新状态不动旧代码"的代码,是检验你设计能力的一道经典题 —— 答案就是状态模式。
给一个工程信号:当你的代码里 if (status == X) 出现超过 5 次,且每个动作里都有类似的 switch 时 —— 状态模式在向你招手。把每个状态对象化,你不只是把 if 搬走了,而是把"状态机"这个隐式存在的概念显式化了。一旦它显式,就能被画出来、被测试、被验证。这就是设计模式从代码升华到设计的力量。
—— 别看了 · 2026