状态模式完全指南:从 if 地狱到订单状态机的优雅演化

"为什么我的代码里 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,但还有 isPaidisShippedisCancelled 等冗余字段,容易不同步。状态字段应该唯一,所有其他派生信息从它推导

坑 2:非法状态可达

没用状态模式时,代码可能允许:状态是 "已发货" 但 paidAt 是 null。状态模式 + 数据库约束 + 业务规则三者一起守护"无效状态不可表示"。

坑 3:状态转移不原子

并发场景下"读当前状态 → 判断 → 写新状态"不原子,两个线程都通过判断都写,bug 出现。修复:用数据库乐观锁(UPDATE ... WHERE status = ? 检查行影响数)或悲观锁(SELECT ... FOR UPDATE)。

坑 4:state 之间共享可变数据

多个 State 实例共享 Context,如果 State 也持有可变字段就乱了。规则:State 对象应该是不可变的(无字段或全 final);真正可变的数据(订单字段)在 Context 里。

识别状态模式场景

  1. 对象有明确的状态(可以列出枚举值)。
  2. 不同状态下相同动作行为不同
  3. 状态转移有明确规则(从哪些状态能转到哪些)。
  4. 状态数量在3 个以上,且能预见会扩展。

写在最后

状态模式是把"过程式的 if 判断"升级成"面向对象的多态分发"的标准操作。它在订单/连接/UI/游戏这些"对象生命周期复杂"的领域里几乎不可或缺。能不能写出"加新状态不动旧代码"的代码,是检验你设计能力的一道经典题 —— 答案就是状态模式。

给一个工程信号:当你的代码里 if (status == X) 出现超过 5 次,且每个动作里都有类似的 switch 时 —— 状态模式在向你招手。把每个状态对象化,你不只是把 if 搬走了,而是把"状态机"这个隐式存在的概念显式化了。一旦它显式,就能被画出来、被测试、被验证。这就是设计模式从代码升华到设计的力量。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

备忘录模式完全指南:从撤销重做到游戏存档与配置回滚

2026-5-15 15:35:28

技术教程

策略模式完全指南:从 if/else 选算法到 Spring 注入与 AB 测试

2026-5-15 15:35:28

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索