你先回忆一下有没有在线上环境改过这样的代码一个订单状态流转的方法里从上到下并排写着十几个 if-else每个分支还要临时去查表当前状态到底能不能走到这个动作。我在一家电商公司刚接手订单模块的时候光是想通已取消的订单能不能再发货这个问题就翻了四五个方法最后发现答案是能因为代码里漏写了一个状态判断。后面我用状态模式把这块逻辑重写了一遍改动的地方反而少了新增一个状态也只是加一个新类。状态模式是 Java 设计模式中典型的行为型模式它解决的核心问题可以概括成一句话允许对象在内部状态改变时改变自身的响应行为看起来就像对象换了类。如果你正在准备 Java 面试、复习软考设计模式速记或者手头正好有一段被 if-else 塞满的状态判断代码这篇文章应该能帮上忙。我会从问题场景讲起用一个订单状态机的完整重构过程把状态模式从思想到代码实现整个过一遍。1. 状态判断写进业务方法之后代码是怎么一步步失控的1.1 一个订单状态判断方法的成长史业务系统里最常见的一种代码演进路径我在很多项目里都见过。最开始状态只有两三个需求也简单直接在 Service 方法里写 if-else 就够了。比如订单有三个状态待支付、待发货、已完成。显示状态描述的时候写一个 getStatusDesc()判断几个 int 就完事执行发货操作时先判断是不是待支付是就抛异常不是就继续。一切看起来都挺清爽。但电商订单的状态远不止这些。加了支付回调就有了待发货加了物流就有了待收货加了售后就有了退款中、退款完成加了平台介入还会有投诉中、处理中。于是那个一开始很清爽的方法开始变味了。我举个例子假设现在订单有五种状态UNPAID待支付、PAID待发货、SHIPPED待收货、COMPLETED已完成、CANCELLED已取消。操作也有五六个包括 pay、ship、confirm、cancel、refund。按 if-else 的写法每个操作对应一个方法而每个方法都要把所有状态的情况判断一遍。一个 ship 方法会变成这样public void ship(Order order) { if (UNPAID.equals(order.getStatus())) { throw new IllegalStateException(未支付不能发货); } if (CANCELLED.equals(order.getStatus())) { throw new IllegalStateException(已取消不能发货); } if (COMPLETED.equals(order.getStatus())) { throw new IllegalStateException(已完成不能发货); } if (SHIPPED.equals(order.getStatus())) { throw new IllegalStateException(不能重复发货); } if (PAID.equals(order.getStatus())) { deliveryService.deliver(order); order.setStatus(SHIPPED); } }每个操作的方法基本都长这样。操作一多这种方法的数量就跟着多起来。你改一个地方要把所有操作都过一遍你加一个状态要把所有操作都加一个判断。这就是大家常说的状态判断散落各处。1.2 if-else 方案的三宗罪可读性、开闭原则、非法流转用 if-else 管状态本质上是在做一件事把当前状态和当前事件两个维度硬编码到一个业务方法里。这个做法有三个很难忽视的缺陷。先说可读性。状态一多每个方法都要把全局状态集合判断一遍读代码的人要不断在方法之间跳转才能拼出完整的流转链路。有时候一个动作的合法状态写在一个很长的条件里后面加需求的人不知道这里埋着判断顺手一改就可能放开一个不该开放的流转。再说开闭原则。为了让一个模块对扩展开放、对修改关闭我们希望加状态时尽量不去动已有代码。但 if-else 方案做不到每加一个新状态所有执行操作的业务方法都要加判断分支每加一个新操作所有状态的合法性检查也要重新过一遍。加需求变成了一件全屋装修式的事情。最后说非法流转的兜底。if-else 漏掉分支的表现通常是静默通过然后执行了一个非法操作把订单打成脏数据。等到下游对账发现问题数据已经错了。我在实际项目里就处理过一次这样的线上事故一个订单被判了已取消但代码里没有拦截重复支付的分支支付回调一来订单状态从已取消直接跳到了待发货账都对不上。这种问题用 if-else 写很难穷举所有组合漏配是迟早的事。1.3 状态模式的核心思想把状态变成对象把行为收回状态自己管说到这里状态模式的答案已经很自然了把状态从 int、String 这种弱类型升级成对象。每个状态对应一个类这个类专门处理当前处于这个状态时各种事件应该怎么办。合法事件执行对应的业务逻辑然后切换到下一个状态非法事件直接拒绝抛出异常。Context 只做转发不负责判断。这种设计把一个全局的、集中的判断拆成了多个状态类内部的局部判断。每个状态类的逻辑都很简单我就是待支付状态我看到 pay 事件就完成支付看到 ship 事件就拒绝。简单意味着好读、好测、好改。在 23 种设计模式里状态模式属于行为型模式关注的是对象间职责的分配。软考设计模式中经常把这个意图和策略模式放一起考很多同学就在这里开始混淆。这个问题我在第四章会专门展开先记住一句话的核心状态模式是状态变了行为跟着变而且状态是自己换的。2. 状态模式的角色拆解谁负责定义、谁负责执行、谁负责切换2.1 State 与 ConcreteState行为按状态划分标准的状态模式有四个参与者State抽象状态定义一组和业务事件对应的方法只声明接口不关心具体实现。ConcreteState具体状态每个状态一个类实现 State 接口把属于自己状态下的事件行为写清楚。Context上下文持有当前 State 引用提供 setState 方法并把客户端请求转发给当前状态对象。Client客户端通过 Context 的方法触发状态流转不直接操作具体状态类。这四者的关系可以这样理解Context 是一台自动售货机State 是售货机当前所处的模式ConcreteState 是某个模式下的内部逻辑。售货机不关心自己处于哪个模式用户投币后它把动作交给当前模式去处理处理完模式自己决定是不是切到下一个模式。需要注意一个容易忽略的点State 接口的方法签名里几乎都要带上 Context。因为具体状态在完成自己的业务动作后要通知这台售货机切换到下一个状态实现方式就是调用 context.setState(nextState)。如果方法参数里没有 Context状态类就没有切换状态的通道状态模式也就名存实亡了。2.2 Context状态持有者与门面Context 是状态模式里唯一被客户端直接操作的类。它要做的事情有三件保存当前状态对象把对外方法转发给当前状态对象提供状态切换入口 setState。一个最简单的 Context 骨架如下public class OrderContext { private OrderState currentState; public OrderContext(OrderState initialState) { this.currentState initialState; } public void setState(OrderState state) { this.currentState state; } public void pay() { currentState.pay(this); } public void ship() { currentState.ship(this); } public void confirm() { currentState.confirm(this); } public void cancel() { currentState.cancel(this); } }看到没有Context 里的每个业务方法只有一行currentState.xxx(this)。真正的逻辑都在状态类里。这就保证了状态相关逻辑是内聚的而不是散落在 Service 层各处。有人可能觉得这样太绕用户调用 pay()Context 先转发给 currentState.pay()currentState 是 UnpaidStateUnpaidState 内部再调用 context.setState(new PaidState())三层跳转。但从维护角度看这种绕是值得的每个状态类都是一个完全独立的小单元你改一个状态不会影响其他状态测试的时候也可以只测单个状态类。2.3 状态流转的两种驱动方式谁来调用 setState实现状态模式时有一个细节会直接影响代码质量状态流转的判断和 setState 调用到底放在哪里。方式一放在 Context 或者外部 Service 里。事件发生时Context 先调用 currentState 的方法然后用一个大的 if-else 或 switch 判断当前状态和事件组合决定下一个状态。这种做法的本质还是全局集中判断只是把 if-else 从业务方法挪到了 Context 里状态类仍然是纯粹的执行器。状态多了之后Context 里的判断并不会比原来少多少只是换了个位置。方式二放在具体状态类内部。每个状态方法在业务动作完成后自己决定并调用 context.setState(nextState)。我推荐这种方式它才是状态模式的精髓。这样每个状态类都完整描述了自己能做什么、不能做什么、做完之后去哪里新增状态时只需要新增一个类并在相关的前置状态里把流转补上改动的范围是局部的。用订单的例子对比UnpaidState 的 pay 方法内部会在支付成功后调用 context.setState(new PaidState())。这段流转逻辑写在 UnpaidState 里读代码的人打开 UnpaidState 就能看到待支付 - 待发货这条链路不需要去 Context 里搜 case也不需要去 Service 层拼状态机。提示状态之间的转移逻辑本身也是一种判断只是从集中的 if-else 换成了分布在各个状态类内部的局部判断。这是一种有意识的换位用分布式的局部判断替代集中式的全局判断换的是可维护性。3. 从 if-else 到状态模式订单状态机重构全程3.1 需求定义与状态矩阵为了演示我设计一个简化但完整的电商订单状态机。订单有 5 个状态4 个核心事件pay、ship、confirm、cancel。先把状态矩阵画出来这一步非常关键它可以避免漏掉某个非法分支的情况状态事件目标状态说明UNPAID待支付payPAID用户支付成功UNPAID待支付cancelCANCELLED未支付直接取消PAID待发货shipSHIPPED商家发货PAID待发货cancelCANCELLED发货前退款取消SHIPPED待收货confirmCOMPLETED用户确认收货COMPLETED已完成任何事件-非法操作CANCELLED已取消任何事件-非法操作其余未列出的组合比如 UNPAID 状态下 ship、PAID 状态下 pay、SHIPPED 状态下 cancel都属于非法操作。在 if-else 版本里非法操作很容易漏写在状态模式版本里它们会成为每个状态类中统一抛异常的方法。3.2 定义 State 接口与 OrderContext先定义 State 接口。方法名直接对应订单的业务事件加上一个 getStateName 方便记录和调试public interface OrderState { void pay(OrderContext context); void ship(OrderContext context); void confirm(OrderContext context); void cancel(OrderContext context); String getStateName(); }再看 OrderContext。和上面的骨架相比我在 setState 里加了一行日志方便在生产环境追踪状态流转路径。如果以后要接审计系统也可以在这里统一埋点public class OrderContext { private OrderState currentState; public OrderContext() { // 订单初始状态待支付 this.currentState new UnpaidState(); } public void setState(OrderState state) { System.out.println(状态变更: currentState.getStateName() - state.getStateName()); this.currentState state; } public void pay() { currentState.pay(this); } public void ship() { currentState.ship(this); } public void confirm() { currentState.confirm(this); } public void cancel() { currentState.cancel(this); } public OrderState getCurrentState() { return currentState; } }有一点要提醒这里构造器直接 new UnpaidState()相当于把初始状态耦合在了 Context 里。如果你的订单可能有多种初始状态比如预订单、直购单建议把初始状态作为构造参数传入或者用工厂方法创建 Context。3.3 实现具体状态类UNPAID 与 PAID接下来是核心部分具体状态类的实现。先看 UnpaidStatepublic class UnpaidState implements OrderState { Override public void pay(OrderContext context) { // 这里写真正的支付回调逻辑校验库存、锁定优惠、调用支付网关等 System.out.println(支付成功订单待发货); context.setState(new PaidState()); } Override public void ship(OrderContext context) { throw new IllegalStateException(未支付订单不能发货); } Override public void confirm(OrderContext context) { throw new IllegalStateException(未支付订单不能确认收货); } Override public void cancel(OrderContext context) { System.out.println(取消订单成功); context.setState(new CancelledState()); } Override public String getStateName() { return UNPAID; } }这个类把待支付状态下所有事件该怎么处理完整表达了出来。合法事件只有 pay 和 cancel非法事件统一抛异常代码里不需要任何 if。UnpaidState 的 pay 方法内部真正支付成功的逻辑执行完后调一下 context.setState(new PaidState())流转完成。再看 PaidState逻辑类似换了一组合法操作public class PaidState implements OrderState { Override public void pay(OrderContext context) { throw new IllegalStateException(订单已支付不能重复支付); } Override public void ship(OrderContext context) { // 调用物流系统写入运单号发通知等 System.out.println(发货成功订单待收货); context.setState(new ShippedState()); } Override public void confirm(OrderContext context) { throw new IllegalStateException(订单尚未发货不能确认收货); } Override public void cancel(OrderContext context) { // 触发退款流程 System.out.println(发货前取消进入退款流程); context.setState(new CancelledState()); } Override public String getStateName() { return PAID; } }这两个类的写法已经能看出状态模式的收益每个类只关注自己的状态域合法/非法行为一目了然不会出现改一个操作把所有状态都牵连一遍的情况。3.4 实现具体状态类SHIPPED、COMPLETED 与 CANCELLEDShippedState 的合法操作只有 confirm其余操作全部拒绝。注意 cancel 这一点实际业务中已发货订单不是不能取消而是要走售后流程所以我在这里直接抛异常把流程分流到售后模块去处理更符合真实业务public class ShippedState implements OrderState { Override public void pay(OrderContext context) { throw new IllegalStateException(订单已支付不能重复支付); } Override public void ship(OrderContext context) { throw new IllegalStateException(订单已发货不能重复发货); } Override public void confirm(OrderContext context) { // 更新订单为完成发放积分等 System.out.println(确认收货订单完成); context.setState(new CompletedState()); } Override public void cancel(OrderContext context) { // 已发货订单不能直接取消走售后流程 throw new IllegalStateException(已发货订单不能直接取消请走售后流程); } Override public String getStateName() { return SHIPPED; } }CompletedState 和 CancelledState 是终态所有事件都非法。在真实项目里终态的每个方法大概率只会抛异常代码重复度会比较高。有同事问过我能不能在抽象父类里把默认抛异常实现掉子类只覆盖合法方法。当然可以这也是常见的优化手段不过要小心一旦默认实现是抛异常子类漏写某个合法事件时不会编译报错而是运行期才发现。我倾向于在状态数量不多时保留显式实现宁可重复几行也不要在状态流转上埋静默漏配的雷。3.5 客户端调用与重构前后对比客户端调用变得非常干净OrderContext order new OrderContext(); order.pay(); // 支付成功订单待发货状态 UNPAID - PAID order.ship(); // 发货成功订单待收货状态 PAID - SHIPPED order.confirm(); // 确认收货订单完成状态 SHIPPED - COMPLETED order.pay(); // 抛 IllegalStateException: 订单已完成不能重复支付我专门跑了一下输入的日志序列是状态变更 UNPAID - PAID、状态变更 PAID - SHIPPED、状态变更 SHIPPED - COMPLETED第四次调用直接抛异常。整个链路完全符合状态矩阵的定义。和 if-else 版本对比差异在下面这张表里能看得很清楚对比维度if-else 方案状态模式方案可读性状态判断散落各方法需全局拼链路每个状态的行为内聚在一个类里扩展性加状态要改所有操作新增状态类 局部流转补充非法操作容易漏写分支静默通过每个状态类显式拒绝不会漏测试每个操作方法要覆盖所有状态每个状态类单独测边界清晰类数量少但代码臃肿多但每个类小而简单如果你正在准备 Java 面试这个对比基本能回答状态模式到底好在哪的追问。面试官更在意的往往不是你背了多少定义而是你有没有见过它真正落地后的样子。4. 状态模式和策略模式结构像双胞胎意图完全不同4.1 为什么这两个模式容易被混在一起状态模式和策略模式在 UML 类图上长得几乎一样都有一个 Context、一个抽象接口、多个实现类。我见过不少人在软考复习时把这两个模式的类图背混了面试里说不清区别的也大有人在。但它们的动机完全相反。策略模式解决的是同一件事可以有多种做法比如支付时选支付宝还是微信、排序时用快排还是归并这些算法彼此独立、可以互换而且通常没有流转关系。状态模式解决的是同一个对象在不同生命周期阶段行为不同状态之间有顺序、有边界而且状态会自己去切换。我总结过一个容易记住的口诀面试和软考都能用策略是换算法状态是换状态策略选给别人看状态流转自己干。4.2 三个判据谁能选、会不会变、谁在换判断一个场景到底该用策略还是状态我一般从三个角度入手第一看行为是谁选的。策略模式里是客户端创建 Context 时主动传入一个策略对象状态模式里客户端通常只看到 Context根本不知道内部现在是哪个状态状态是内部自己维护的。第二看状态之间会不会自己流转。策略模式中策略对象不会把自己换成另一个策略状态模式中状态对象会在事件触发后调用 context.setState这是状态模式最典型的特征。第三看行为是否随历史变化。策略模式里同一策略执行结果相对稳定状态模式里同一个事件在不同状态下结果完全不同甚至合法/非法都会改变这和对象当前所处生命周期息息相关。下面这个表格适合做复习卡片对比维度策略模式状态模式核心意图封装一族可替换的算法根据状态改变对象行为切换主体客户端主动选择状态对象内部自流转状态意识无状态记录有明确状态与流转客户端感知通常要感知具体策略不需要感知具体状态类之间关系互相独立有流转依赖4.3 误用状态模式写策略的后果我在 Code Review 里见过一个反面例子正好能加深理解。有个同事要做订单金额计算按用户等级用不同的折扣规则他把每种折扣规则写成了状态类并且在状态类里互相 setState结果发现计算一次金额折扣对象还要根据金额大小切换状态越写越乱。这就是典型的用状态模式硬套策略场景。正确做法应该是定义一个 PriceStrategy 接口客户端把具体的折扣策略注入 Context。策略之间不需要知道彼此存在也不需要任何流转状态模式里那套状态自己切换的机制在这里反而是负担。如果你在面试中被问到实际项目中怎么快速分辨我的回答通常是先看这个对象会不会在运行期自动改变自己的分类。订单会从待支付变成待发货这是自动流转但支付方式不会自己从支付宝变成微信它是外部换的。自动流转的那部分才需要状态模式。5. 实践避坑状态对象复用、流转归属和多线程安全5.1 状态实例每次 new 还是缓存复用代码照上面写能跑但有个小问题每发生一次状态流转就在 setState 里 new 一个状态对象。状态类的实例通常不持有业务数据它是典型的无状态对象每次 new 会产生不必要的垃圾对象。项目规模小无所谓但订单这种高并发场景频繁 new 对象会带来明显的分配压力。更常见的做法是让每个状态类变成单例Context 里直接复用。我在一个中大型项目里把状态类从每次 new改成了静态实例引用用 profiler 看对象分配数量肉眼可见地降了一截。改造很简单把构造器私有化暴露一个静态常量即可public class UnpaidState implements OrderState { public static final UnpaidState INSTANCE new UnpaidState(); private UnpaidState() { } // 其余方法不变 }不过要记住一个前提状态类内部不能有可变成员变量。如果状态类里存了订单号、用户 ID 这类业务数据共享单例就会出线程安全问题。状态模式里业务数据应该放在 Context 侧状态类只负责行为逻辑。那种状态类里存数据的设计通常是思路还没转过来。更彻底的做法是直接用枚举实现状态类。枚举天然是单例还可以带方法代码会更紧凑很多开源状态机就是这么设计的。枚举不适合的是状态行为特别复杂、一个状态要实现几十行逻辑的场景那种情况下拆成独立类更好阅读。5.2 状态流转的幂等与非法操作兜底非法操作是抛异常还是静默忽略要结合业务来判断。订单类业务我建议抛异常宁可让上层捕获处理也不能让一个已取消的订单被重复支付成功。但某些异步场景比如消息队列重复投递事件本身可能重复触发这时候状态机的入口就要做幂等处理如果收到的事件和当前状态不匹配直接返回成功而不是抛异常。我在实际项目中踩过一次这样的坑支付回调接口被消息队列重放了两次第一条消息把订单从 UNPAID 推进到 PAID第二条消息进来时状态已经是 PAID代码里直接抛了重复支付异常。看起来没错但支付平台那边把异常当成处理失败又继续重试导致告警刷屏。后来我在入口做了幂等判断如果订单已经是 PAID再次收到支付成功消息直接返回成功不落状态机。这个经验说明状态模式只负责组织代码幂等和重试策略还是要结合业务单独设计。5.3 多线程下的 Context 状态切换同一个订单对象如果可能被并发操作Context 里的 currentState 字段建议至少用 volatile 修饰避免一个线程看到另一个线程切换了一半的引用public class OrderContext { private volatile OrderState currentState; // ... }但 volatile 只能保证可见性保证不了支付和取消两个操作在同一时刻只有一个能推进状态。真实场景下两个请求同时操作同一个订单的情况虽然少一旦发生就比较麻烦。服务端一般会用数据库乐观锁收口更新订单状态时带上版本号只有版本号匹配才更新成功。状态模式解决的是应用层的代码组织问题事务和并发一致性仍然要靠锁、版本号、事务这些手段。顺便说一句状态模式本身并不适合承担分布式状态机的职责。如果订单状态要跨服务流转或者状态判断要基于数据库里的历史记录更合适的做法是接一个独立的状态机组件或者把状态存储在持久层里应用层的状态对象只做当前进程内的缓存映照。这个问题我早期没想明白把状态对象放 Redis 里硬存结果序列化和反序列化非常折腾后来改成持久化状态枚举 应用层状态模式两边都清爽了。5.4 别用状态模式硬套简单场景状态模式最大的坑可能是为了用而用。状态少于三个、流转路径固定不变、以后也不太可能新增状态时老老实实用 if-else 或枚举加 switch 就行。状态模式的价值体现在状态数量多、流转复杂、变化频繁的场景里。我见过为了展示设计模式把两个状态也拆成四个类的代码阅读成本比收益还大。我现在的习惯是拿到一个状态相关的需求先画状态矩阵再决定要不要用状态模式。如果矩阵只是两行三列直接用 if如果矩阵到了五行四列而且还有各种非法分支果断状态模式。这张状态矩阵本身就是最好的设计文档画完矩阵再写代码基本不会漏流转。踩过几次坑之后我对状态模式的态度是这样的它是一种组织代码的方式不是用来炫技的道具。状态模式的真正价值是让状态相关的逻辑收敛到局部、让未来加需求的人不用翻遍整个 Service 层、让非法流转在编译期和测试期就被显式暴露。能做到这一点它作为行为型设计模式的使命就完成了做不到或者场景根本不复杂那别硬上简单代码也是一种设计。