状态模式要求用类型安全的枚举定义离散状态,通过受控转换方法和守卫条件约束状态流转,各状态类职责单一、不持业务数据,且状态变更与持久化须事务一致。

状态模式不是给 if-else 加个壳,而是把「状态决定行为」这件事从代码里拎出来,让每个状态自己管自己的逻辑。它真正起效的前提是:订单存在明确、不可逆(或有严格守卫)的状态序列,且不同状态下能响应的事件有限。
订单状态必须建模成离散、可枚举的值
硬编码数字或字符串状态(如 state = 2 或 state == "paid")会立刻让状态机失控。必须用类型安全的方式约束:
- Java 用
enum OrderStatus { UNPAID, PAID, SHIPPED, COMPLETED, CANCELLED },所有转换只能在这几个值之间发生 - Python 用
Enum类,避免字符串拼写错误导致状态“消失” - 禁止在业务逻辑中直接赋值
order.status = "shipped",必须通过order.transition_to(OrderStatus.SHIPPED)这类受控方法 - 状态枚举本身要包含合法前驱状态(例如
SHIPPED只允许从PAID转入),否则校验逻辑会散落在各处
每个具体状态类只处理它该响应的事件
别让 UnpaidState.handle() 去判断“如果当前是已发货状态就报错”——那是上下文或状态转换守卫该干的事。具体状态类的职责非常窄:
-
UnpaidState.handle_payment()只做三件事:校验支付参数、调用支付网关、成功后通知上下文切换到PaiedState -
ShippedState.handle_receive()只检查是否超时、更新收货时间、触发积分发放,不碰库存或风控 - 任何跨状态的副作用(比如发货时扣库存)必须放在状态切换的钩子(hook)里,而不是塞进某个 handle 方法中
- 如果某个事件在多个状态下行为一致(如
cancel在未支付和已发货时都需冻结资金),说明你漏掉了共用的父状态或策略提取点
状态转换必须由守卫条件(guard condition)控制
没有守卫的状态机等于裸奔。常见错误是把校验逻辑写在事件处理器里,导致非法转换仍能发生:
- 错误写法:
if (event == PAY && order.isBalanceSufficient()) { order.setState(PAID); }—— 状态变更发生在条件内部,难以统一拦截 - 正确做法:在
OrderContext.transition_to(new PaiedState())前,由PaiedState.can_enter_from(currentState)返回false拦截 - 所有守卫条件必须集中定义,例如用矩阵表或 Map<OrderStatus, Set<OrderStatus>> 显式声明允许的流入路径
- 风控拦截、库存不足这类外部失败,应抛出特定异常(如
InsufficientStockException),而非返回布尔值,避免被忽略
状态对象不能持有业务数据引用
具体状态类(如 ShippedState)如果直接持有了 Order 实例或 DAO,就会变成状态逻辑和数据访问的混合体,违背单一职责:
- 状态类只通过上下文(
OrderContext)提供的只读接口获取必要字段,例如context.getOrderId()、context.getLatestTrackingNo() - 所有写操作(更新数据库、发消息、调外部 API)必须由上下文委托给独立的服务层,状态类只负责决策“要不要做”和“传什么参数”
- 测试时能轻松 mock 上下文行为,而不用启动整个订单服务
- 一旦发现某个状态类里出现了
orderDao.update(...)或jdbcTemplate.update(...),就是重构失败的信号
最常被跳过的一步是:状态变更后的持久化时机。很多人以为状态对象切换完就结束了,但数据库记录可能还是旧值——必须确保状态流转和 DB 更新在同一个事务内完成,且顺序不能颠倒。这通常需要在上下文的 transition_to() 方法里统一埋点,而不是靠每个状态类自己去 save。

















