← 返回题目列表

如何手写一个状态模式?核心代码怎么设计?

高频 中等 第 3 / 25 题 更新于 2026/07/28
状态模式代码实现Java状态迁移设计模式

简化版

手写状态模式通常先定义 State 接口,再为每个状态写一个 ConcreteState 类,然后让 Context 持有当前 State,并把操作委托给当前状态对象。状态对象根据业务规则执行行为,并在需要时切换 Context 的当前状态。

详细版

实现状态模式可以按四步走:

  1. 定义状态接口,列出可能的操作;
  2. 为每个具体状态实现状态类;
  3. Context 持有当前状态;
  4. Context 的业务方法委托给当前状态对象。

简化代码:

interface OrderState {
    void pay(OrderContext context);
    void cancel(OrderContext context);
}

class OrderContext {
    private OrderState state;

    public void pay() {
        state.pay(this);
    }

    public void setState(OrderState state) {
        this.state = state;
    }
}

具体状态类负责当前状态下的行为和迁移。真实项目里还要考虑状态持久化、非法操作返回、并发更新、状态迁移日志和事务一致性。

完整版教学

一、先定义订单状态接口

状态接口要覆盖系统可能收到的操作。

interface OrderState {
    void pay(OrderContext context);
    void cancel(OrderContext context);
    void ship(OrderContext context);
    void finish(OrderContext context);
}

注意,这里不是说每个状态都允许这些操作,而是系统可能对订单发起这些操作。每个状态类自己决定允许还是拒绝。

二、实现待支付状态

class WaitPayState implements OrderState {
    public void pay(OrderContext context) {
        System.out.println("支付成功");
        context.setState(new PaidState());
    }

    public void cancel(OrderContext context) {
        System.out.println("取消订单");
        context.setState(new CanceledState());
    }

    public void ship(OrderContext context) {
        throw new RuntimeException("待支付订单不能发货");
    }

    public void finish(OrderContext context) {
        throw new RuntimeException("待支付订单不能完成");
    }
}

待支付状态只处理自己状态下的规则:可以支付,可以取消,不能发货,不能完成。

三、实现已支付状态

class PaidState implements OrderState {
    public void pay(OrderContext context) {
        throw new RuntimeException("不能重复支付");
    }

    public void cancel(OrderContext context) {
        System.out.println("退款并取消订单");
        context.setState(new CanceledState());
    }

    public void ship(OrderContext context) {
        System.out.println("发货成功");
        context.setState(new ShippedState());
    }

    public void finish(OrderContext context) {
        throw new RuntimeException("未发货不能完成");
    }
}

已支付状态下,发货是合法迁移,重复支付是非法操作。

四、Context 负责统一入口

class OrderContext {
    private OrderState state = new WaitPayState();

    public void pay() {
        state.pay(this);
    }

    public void cancel() {
        state.cancel(this);
    }

    public void ship() {
        state.ship(this);
    }

    public void finish() {
        state.finish(this);
    }

    public void setState(OrderState state) {
        this.state = state;
    }
}

客户端始终调用 OrderContext,不需要关心当前状态。

五、真实项目里状态通常不能只放内存

上面的例子适合讲原理,但真实订单状态通常要存数据库。

例如:

订单表 status 字段
状态变更日志表
业务操作事务

状态对象里的 context.setState() 不能只改内存,还要考虑:

  • 数据库状态更新;
  • 乐观锁防并发修改;
  • 状态变更日志;
  • 消息发送;
  • 事务回滚。

否则应用重启后状态就没了,或者并发请求把状态改乱。

六、非法操作要有统一处理

状态模式里很多方法在某些状态下不允许执行。

不要每个状态类随便抛不同异常。工程里最好定义统一的非法状态操作异常或统一错误码。

比如:

throw new IllegalStateTransitionException("待支付订单不能发货");

这样上层可以统一转换成接口响应。

七、状态对象是否可以复用

如果状态类本身没有成员变量,只包含行为逻辑,可以设计成单例,避免频繁创建对象。

比如:

enum OrderStates {
    WAIT_PAY(new WaitPayState()),
    PAID(new PaidState());
}

但如果状态对象持有请求级数据,就不能做成共享单例。更推荐让状态对象无状态,把业务数据放在 Context 中。

八、状态对象复用与持久化一致性

无字段的 PaidState 可作为单例供 1000 个订单复用;订单 id、金额等请求数据必须留在 Context 或参数中,不能写入共享状态对象。

load status -> map to stateless State -> handle event -> CAS persist -> side effects

状态题的正确性落在“当前状态 + 事件 + 守卫条件 → 目标状态”这一条边上,而不是类数量上。每条边都要说明持久化原子性、重复事件结果和副作用触发时机,非法边则必须得到稳定且可观测的拒绝。

九、迁移一致性、幂等与并发验证

检查维度应确认的内容
机制正确性状态类应尽量无状态,真正的订单数据属于聚合,不属于共享状态策略对象。
适用边界示例只改内存适合教学,生产系统还要把状态码持久化,并用事务或乐观锁防止并发覆盖。
测试证据覆盖正常迁移、重复事件、非法迁移、持久化失败和两个请求竞争同一版本
工程代价重点评估状态数、迁移边数、一次迁移的数据库竞争,以及新增状态对完整迁移矩阵的影响

易错点:状态类应尽量无状态,真正的订单数据属于聚合,不属于共享状态策略对象。

十、常见误区与追问

  • 误区:每个订单都必须 new 一套状态对象。 无请求数据的状态实现可安全复用,减少对象创建;有可变字段则要重新设计。
  • 误区:把状态码换成状态类就自动消除了所有条件判断。 状态对象只能封装状态相关行为;持久化、并发控制、权限和跨聚合规则仍需显式设计。
  • 追问:数据库更新失败后能先改内存状态吗? 不能把失败更新当成功,应以持久化结果为准,必要时回滚内存或重新加载聚合。
  • 追问:非法状态迁移应该静默忽略吗? 不应该;应返回明确业务错误并记录当前状态、事件和聚合标识,重复事件则按幂等规则单独处理。
  • 追问:状态模式最容易漏掉哪类测试? 非法迁移、重复事件和并发竞争;它们比单线程成功路径更能证明状态模型可靠。

十一、加强记忆

手写状态模式记住四步:State 接口定义操作,ConcreteState 封装各状态行为,Context 持有当前状态,客户端只调用 Context。真实项目还要补充状态持久化、并发控制、非法操作统一处理和状态变更日志,这些才是工程落地的关键。