1. 先还原一个让人头大的订单状态if-else场景1.1 一段真实到落泪的订单状态代码周五下午四点运营同事跑过来说需要在订单后台加一个退款中状态。我打开订单模块的代码看着那段熟悉又窒息的状态判断逻辑心里已经预感到这个周末要搭进去了。如果你写过几年业务代码大概率见过类似的写法public void processOrder(String state, Order order) { if (UNPAID.equals(state)) { // 待支付可以支付可以取消 if (pay(order)) { order.setState(PAID); } cancel(order); } else if (PAID.equals(state)) { // 已支付可以发货可以退款 if (refund(order)) { order.setState(REFUNDING); } else { ship(order); order.setState(SHIPPED); } } else if (SHIPPED.equals(state)) { // 已发货可以确认收货 if (confirmReceive(order)) { order.setState(COMPLETED); } } else if (COMPLETED.equals(state)) { // 已完成可以评价 evaluate(order); } // 后来又加了 REFUNDING、REFUNDED、CLOSED... }这段代码有三个让人头大的地方。第一状态判断散落在各个业务方法里。今天加一个退款中明天加一个售后中每加一个状态就要在所有相关方法里检查一遍漏掉一个判断就会出现订单已经退款了还在发货的诡异场景。第二状态和行为高度耦合。processOrder这个方法不断膨胀最后变成几百行的上帝方法读代码的人找不到重点。第三状态之间没有明确的流转边界。任何一个if分支都可以随手把订单状态改成任意值没人能保证流转合法。1.2 问题背后的本质状态判断和业务行为耦合在一起很多新人以为这只是代码写得丑的问题优化一下if-else结构就能解决。但实际上这就是典型的状态和行为耦合问题。对象的状态和行为本来是两个维度。订单处于不同状态时同样一个支付动作在待支付状态是合法操作在已完成状态就是非法操作。当你用if-else写判断时本质上是把这些状态维度的逻辑全部压平到一个方法里靠条件分支来区分。这种写法的最大问题不是代码冗长而是违背了开闭原则——新增一种状态就要修改大量既有判断逻辑改来改去难免动到原有分支。当你一遍遍在这些if-else里打补丁的时候说明状态模式已经该登场了。1.3 状态模式什么时候入场状态模式适用的场景非常明确一个对象的行为取决于其当前状态而且状态切换时行为会发生剧烈变化。简单说就是——如果你的代码里充满当状态是A时做P当状态是B时做Q当状态是C时做R这样的逻辑而且状态数量还在不断增加那状态模式就是最自然的解法。它和策略模式模板方法这类模式的不同在于状态模式把状态和行为的映射固化到了类结构里让代码结构本身成为一张状态流转表根本不需要人工维护那堆分散的条件判断。引用状态模式的定义就是允许一个对象在其内部状态改变时改变它的行为对象看起来似乎修改了它的类。这句话听起来有点绕翻译成大白话就是把每一个状态封装成独立对象状态之间的切换由状态对象自己驱动行为跟着状态走调用方根本不需要关心当前是什么状态。2. 状态模式的核心拆解状态变成对象行为跟着状态走2.1 一句话理解状态模式还是用订单系统举例。传统if-else做法的隐含逻辑是因为订单此时是已支付状态所以我要执行发货操作。状态模式的做法是订单当前这个状态对象自己知道我该做什么。打个比方你在一家餐厅点菜。传统做法是服务员拿着一个巨大的菜单根据你的身份状态查表——你是会员就给你打八折你是新客就给你送甜点你是黑名单客户就让你直接走人。状态模式的做法是把每一个客户类型变成一个相对独立的服务员角色会员服务员、新客服务员、黑名单服务员各自按自己的规则接待你大门口根本不需要堆一个几百行的判断逻辑。这背后是面向对象设计里一个很重要的思想把变化的东西封装起来。状态是变化的行为是随状态变化的那就让状态自己负责自己的行为而不是让外部环境来猜。2.2 三个角色的定义与职责状态模式的结构非常清晰只有三个角色比很多设计模式都好记。角色职责生活类比Context环境类持有当前状态对象把操作请求转发给状态对象遥控器或者操作面板State抽象状态接口定义该状态下可执行的所有行为的方法签名一揽子规则清单ConcreteState具体状态类实现某个特定状态下的具体行为并决定是否切换到其他状态各种具体的规则执行者这里面最关键的设计点是Context本身不感知具体状态逻辑它只是持有一个State接口的引用。每次客户端调用Context上的方法时Context直接转交给自己手中那个状态对象去处理。状态对象处理完之后可以调用Context的setState方法实现状态切换而这个切换动作对外部调用方完全透明。打个简单的比方你在自动售货机上按可乐售货机不会在内部写if(投币) 出可乐; else if(缺货) 退款这种逻辑——它会直接问当前这个状态对象投币状态/缺货状态/正常状态该怎么处理。这样理解状态模式就很直观了。2.3 为什么对象看起来似乎修改了它的类这句话是GoF原书里写的第一次看非常疑惑——一个对象怎么修改自己的类其实它说的是运行时行为变化不是编译期的类型变化。当你给Context切换状态对象时Context对外暴露的行为比如支付、发货、取消可能完全不同了在UnpaidState下调用pay()会成功并且状态转为PAID在CompletedState下调用pay()会直接抛异常。同一个对象同一段调用代码得到的结果不一样从外部观察者的视角看就好像这个对象换了一个类一样。理解了这一层你才算真正理解了状态模式为什么叫行为型模式。3. Java代码实现订单从待支付到已完成的完整状态机3.1 定义State接口与订单上下文我看很多教程演示状态模式都喜欢用电灯开关这种极简例子确实好懂但离真实业务有点远。这里我用订单状态流转来写一个完整可运行的Java实现直接覆盖待支付→已支付→已发货→已完成的核心链路再加上已取消这个分支终点。先写State接口public interface OrderState { // 支付操作 void pay(OrderContext context); // 发货操作 void ship(OrderContext context); // 确认收货/完成操作 void complete(OrderContext context); // 取消操作 void cancel(OrderContext context); }有人可能会问为什么把各个操作都放进状态接口里难道每个状态类都要实现所有方法对这正是状态模式的特点——接口是全操作集具体状态里不能执行的操作就抛异常或者给一个默认提示。这样设计的好处是Context和调用方永远使用同一套方法签名不需要做任何类型判断。接着写OrderContextpublic class OrderContext { private OrderState currentState; private String orderNo; public OrderContext(String orderNo) { this.orderNo orderNo; // 初始状态待支付 this.currentState new UnpaidState(); } public OrderState getCurrentState() { return currentState; } // 状态切换的入口由状态类调用 public void setState(OrderState state) { System.out.println(订单[ orderNo ]状态切换: currentState.getClass().getSimpleName() - state.getClass().getSimpleName()); this.currentState state; } // 以下都是对当前状态对象的委托调用 public void pay() { currentState.pay(this); } public void ship() { currentState.ship(this); } public void complete() { currentState.complete(this); } public void cancel() { currentState.cancel(this); } }Context里的setState方法尤其重要它是整个状态机里唯一被允许改变状态的入口。这样做有一个很大的好处——状态流转路径可以被统一监控和约束比如加日志、做审计、记账都在这一处完成不用到处埋点。3.2 实现具体状态类接下来就是重头戏具体状态类。我把每个状态类都写成独立的类各自实现自己在当前状态下对各个操作的反应。首先是UnpaidState待支付状态public class UnpaidState implements OrderState { Override public void pay(OrderContext context) { System.out.println(支付成功订单状态变为已支付); context.setState(new PaidState()); } Override public void ship(OrderContext context) { System.out.println(非法操作订单尚未支付不能发货); throw new IllegalStateException(订单尚未支付); } Override public void complete(OrderContext context) { System.out.println(非法操作订单尚未支付不能完成); throw new IllegalStateException(订单尚未支付); } Override public void cancel(OrderContext context) { System.out.println(订单未支付直接取消); context.setState(new CancelledState()); } }然后是PaidState已支付状态public class PaidState implements OrderState { Override public void pay(OrderContext context) { System.out.println(非法操作订单已支付不能重复支付); throw new IllegalStateException(订单已支付); } Override public void ship(OrderContext context) { System.out.println(发货成功订单状态变为已发货); context.setState(new ShippedState()); } Override public void complete(OrderContext context) { System.out.println(非法操作订单未发货不能完成); throw new IllegalStateException(订单未发货); } Override public void cancel(OrderContext context) { System.out.println(已支付订单申请取消进入退款流程); context.setState(new RefundingState()); } }再写ShippedState已发货状态和CompletedState已完成状态public class ShippedState implements OrderState { Override public void pay(OrderContext context) { System.out.println(非法操作订单已发货无需支付); throw new IllegalStateException(订单已发货); } Override public void ship(OrderContext context) { System.out.println(非法操作订单已发货不能重复发货); throw new IllegalStateException(订单已发货); } Override public void complete(OrderContext context) { System.out.println(确认收货订单完成); context.setState(new CompletedState()); } Override public void cancel(OrderContext context) { System.out.println(非法操作订单已发货不能取消); throw new IllegalStateException(订单已发货); } } public class CompletedState implements OrderState { Override public void pay(OrderContext context) { throw new IllegalStateException(订单已完成); } Override public void ship(OrderContext context) { throw new IllegalStateException(订单已完成); } Override public void complete(OrderContext context) { System.out.println(订单已完成不能重复完成); throw new IllegalStateException(订单已完成); } Override public void cancel(OrderContext context) { throw new IllegalStateException(订单已完成); } }为了不让状态链路过于简单我再加一个RefundingState退款中状态它代表从PaidState取消之后进入的中间状态最终可以退成RefundedState也可以退款失败后回到PaidState。这样可以让状态模式的自流转特点体现得更充分。public class RefundingState implements OrderState { Override public void pay(OrderContext context) { throw new IllegalStateException(退款中不能支付); } Override public void ship(OrderContext context) { throw new IllegalStateException(退款中不能发货); } Override public void complete(OrderContext context) { throw new IllegalStateException(退款中不能完成); } Override public void cancel(OrderContext context) { System.out.println(退款完成订单已关闭); context.setState(new RefundedState()); } } public class RefundedState implements OrderState { Override public void pay(OrderContext context) { throw new IllegalStateException(订单已退款关闭); } Override public void ship(OrderContext context) { throw new IllegalStateException(订单已退款关闭); } Override public void complete(OrderContext context) { throw new IllegalStateException(订单已退款关闭); } Override public void cancel(OrderContext context) { System.out.println(订单已处于退款关闭状态); throw new IllegalStateException(订单已退款关闭); } }3.3 用一段测试代码把状态流转跑起来状态类都写好后跑一段简单的测试代码看看效果public class StatePatternDemo { public static void main(String[] args) { OrderContext order new OrderContext(SO20240815001); // 待支付状态直接取消 order.cancel(); // 新订单走到正常流程 OrderContext order2 new OrderContext(SO20240815002); order2.pay(); // 待支付 - 已支付 order2.ship(); // 已支付 - 已发货 order2.complete(); // 已发货 - 已完成 // 试试非法操作 OrderContext order3 new OrderContext(SO20240815003); order3.ship(); // 待支付时直接发货抛异常 } }输出效果类似如下订单[SO20240815001]状态切换: UnpaidState - CancelledState 订单未支付直接取消 订单[SO20240815002]状态切换: UnpaidState - PaidState 支付成功订单状态变为已支付 订单[SO20240815002]状态切换: PaidState - ShippedState 发货成功订单状态变为已发货 订单[SO20240815002]状态切换: ShippedState - CompletedState 确认收货订单完成 Exception in thread main java.lang.IllegalStateException: 订单尚未支付这段执行过程非常直观地展示了状态模式的效果整个订单操作没有任何一个if-else判断但非法操作全部被拦住了。每个状态类就是当前状态下的一本操作手册越界操作直接抛异常合法操作则触发下一步的状态流转。3.4 对这段实现的两个关键解释第一个关键点是每个具体状态类只关心自己状态下的行为。UnpaidState里没有发货的任何逻辑它只会告诉你不允许发货PaidState里没有确认收货的逻辑只有ShippedState才有。当新需求要加一个新状态时比如待评价你只需要新增一个PendingReviewState然后在CompletedState里把流转目标从直接结束改成进入这个新状态即可根本不需要在主流程里插入大段判断。第二个关键点是状态切换的控制权在状态类手里但状态存储中心在Context里。setState方法在Context中状态对象通过传入的context参数来触发切换。这种协作方式保证了谁执行完动作谁负责把状态交给下一个状态。4. 状态流转的控制权之争到底谁负责切换状态4.1 方案A状态由Context统一调度我在3.x版本的状态类代码里用的是状态类内部自流转的方案——状态对象处理完自己的逻辑后直接调用context.setState(new NextState())。这是状态模式最经典的写法也是大多数教材的默认实现。但这个方案有人会质疑状态类之间互相耦合了。UnpaidState直接new了一个PaidStatePaidState直接new了一个ShippedState。一旦状态之间的流转关系复杂起来这堆new操作散布在各个状态类里系统的流转路径就变得不那么一目了然。于是就有了方案B把状态流转逻辑集中到Context里。典型的做法是Context维护一个状态子类注册表或者一张流转表然后调用状态类的方法时由Context去判断执行完之后应该切到哪个状态。这种方式更像状态机框架比如Spring StateMachine就是这种思路把状态流转都定义在配置里状态类本身不关心下一个状态是谁。4.2 方案A和方案B怎么选这两种方案没有绝对的对错只有适不适合当前业务。我自己的经验是用一张表格总结比较直观维度方案A状态类自流转方案BContext统一流转状态切换的可读性流转分散在各状态类中需要通读所有类才能看清全貌集中在一处配置全貌清晰状态类的独立性状态类知道下一个状态耦合度高状态类只知道自己的行为不知道下一个是谁新增状态的开销需要改旧状态类中的流转代码只需要改Context的配置但可能也要改状态类行为适用场景状态数量少、流转路径固定状态数量多、流转复杂、需要可视化代码规模小适合快速开发需要额外设计流转表或注册表代码量偏大我实际做订单模块重构的时候初期用方案A因为状态就五六个流转路径一眼能看完。后来状态增加到十几个、出现了各种分支路径之后方案A就开始让人头大了——要找一条完整路径得翻五六个类文件。这时候才意识到状态模式里的状态流转表本身也是一等公民应该被单独建模于是改成方案B用一张Map当前状态, 事件 - 目标状态的表来驱动代码反而清爽很多。4.3 工程上的另一个优化状态实例单例化不管你用方案A还是方案B还有一个细节值得注意ConcreteState是无状态的可以单例共享。仔细看我的示例代码每个状态类里有没有成员变量没有。状态类的方法参数里都带着OrderContext需要的数据全部通过Context传入所以理论上同一时刻只有一个UnpaidState实例在处理业务也完全没问题。如果你每次切换状态都new一个新对象在高并发大流量的订单系统里大量状态对象被频繁创建销毁对JVM的GC压力不小。优化很简单把状态类做成单例public class UnpaidState implements OrderState { // 私有构造 静态实例 private static final UnpaidState INSTANCE new UnpaidState(); private UnpaidState() {} public static UnpaidState getInstance() { return INSTANCE; } Override public void pay(OrderContext context) { // 业务逻辑... context.setState(PaidState.getInstance()); } }这样上下文持有的是同一个状态实例内存里每种状态只存在一份对象。这个优化在真实项目里非常常见面试时如果主动提出来面试官通常会比较认可——说明你真的在工程环境里考虑过资源开销。不过注意一点如果你在状态类里引入了可变的成员变量比如统计计数那单例化就是灾难并发环境下数据全乱了。所以单例化的前提是状态类必须无状态所有可变数据一律放在Context里。5. 最容易混淆的兄弟模式状态模式 vs 策略模式5.1 类图几乎一样意图完全相反状态模式是23种设计模式里坑最多、最容易和策略模式混淆的一个。因为两者的类图结构几乎一模一样——都有一个Context都有一个策略/状态接口都有一堆实现类。很多人刚学的时候直接看类图根本分不清谁是谁。两类模式的类图对比维度状态模式策略模式核心意图对象在不同状态下自动改变行为状态可以自行切换封装不同算法/策略让客户端选择使用哪一个状态的变更者状态对象自己驱动或Context统一驱动状态会自动流转客户端主动指定使用哪种策略策略不互相切换对客户的感知客户不知道、也不关心当前是哪个状态客户明确知道自己选择的是哪种策略状态/策略之间互相有流转关系构成状态机互相独立没有流转关系典型用例订单状态机、工作流审批、电梯运行排序算法选择、支付方式选择、压缩算法举一个最容易记的对比例子。拿支付方式来说客户选择用支付宝还是微信支付这是策略模式——客户自己选选完就用两个策略之间毫无关系。订单状态从待支付变成已支付再变成已发货这是状态模式——状态是自动流转的你支付成功后订单自己就知道要进入已支付状态根本不需要客户告诉它下一步是什么状态。5.2 两条容易记混的口诀既然大家考试、面试都要记口诀我就送两条我自己的土办法。第一条策略客户说了算状态事情自己变。策略模式永远是调用方在选状态模式的Context往往只是被动接招状态自己在动。第二条策略是并列关系状态是前后关系。策略实现类之间是我选A就不选B的并列关系状态实现类之间是我从A走到B再到C的先后关系。看类与类之间有没有流转连接就能快速判断是状态模式还是策略模式。这两条不仅考试有用写代码的时候也很有用——当你发现自己在一个策略实现类里开始写如果当前状态是X就切到Y这种代码那说明你已经不是在写策略模式了。6. 实战要点、注意事项与面试/软考答题口径6.1 状态模式的适用边界哪些场景别硬用状态模式很好用但也不是万金油。我见过不少新人学了状态模式之后什么代码都想往里面套结果把简单问题搞复杂了。以下几个信号告诉你不建议用状态模式状态数量很少且永久固定。三四个常量状态用枚举加简单的条件判断反而更清晰。状态模式引入的类数量至少是状态数2接口Context状态少的时候有点杀鸡用牛刀。状态之间的行为差别不大。如果各个状态只是标记一下而已没有具体的行为分化用状态模式纯粹是增加类文件数量。状态流转逻辑非常动态无法预先确定。状态模式里的流转路径是在代码里写死的如果状态切换取决于复杂的运行期条件、用户输入等硬往状态模式上套反而更难维护。反过来如果代码里频繁出现根据状态去判断分支执行不同行为的逻辑并且状态数量在业务演进中持续增加那状态模式就很适合。6.2 实战中的细节坑并发、次数、状态持久化第一是并发安全问题。状态切换不是原子操作——状态类调用context.setState()之前可能有另一个线程已经切走了状态。订单场景还好因为一个订单的处理并发度低但如果是账户状态、会话状态这类高并发场景就要考虑在切换状态时加锁或者设计成版本号条件更新的方式确保状态切换的一致性。第二是状态次数与幂等。有些状态不能凭空发生两次比如complete()重复调用必须抛异常而不是静默成功。状态模式的终态类设计上天生就有这个好处但前提是你保持了非法操作就抛异常的风格。如果图省事在终态里写一个return默默吞掉后续排查问题会非常痛苦。第三是状态持久化与恢复。状态模式的Context持有的是Java对象状态一旦JVM重启内存里的状态就没了。真实系统里订单状态肯定是存数据库的所以不要把状态模式的状态理解成只存在于内存而是应该把当前状态映射成数据库中一个字段。加载订单时从数据库读出状态字符串再用工厂方法映射成对应的状态对象。这个映射工厂可以简单用switch也可以用到后面会讲的简单工厂模式。6.3 软考与面试的高频问法如果是要备考软考软件设计师或者应付面试状态模式有几种典型问法值得提前准备好。问状态模式的主要意图是什么标准答法允许一个对象在其内部状态改变时改变它的行为让对象看起来似乎修改了它的类。将状态判断逻辑从庞大的条件分支中抽离封装到独立状态类中符合开闭原则。问状态模式和策略模式有什么区别这是出镜率最高的问题。答题要点结构相似但意图不同。策略模式的策略由客户端选择且策略间相互独立状态模式的状态自动流转且状态间存在切换关系。一句话收尾策略解决怎么算的算法替换状态解决现在能干吗的行为切换。问状态模式有哪些缺点诚实答法一是类数量随状态增多而膨胀二是状态间的流转逻辑仍然分散尤其方案A中全流程可读性下降三是如果状态切换逻辑频繁变更维护成本不低。答出缺点反而加分说明不是背书的。记忆口诀方面我的建议是别硬记状态模式行为型模式这种零散信息而是记一条链路行为模式状态机Context持当前状态自切换行为差异大。考试时先判断是否状态驱动行为再判断是否自动流转基本不会答错。写在最后最后分享一个个人实战中的小技巧。我在重构某个订单状态模块的时候把状态模式的setState方法里加了一行日志输出完整的流转路径然后再配上一张状态流转表放到项目文档里。后来不管是新同事接手还是线上排查问题都靠这行日志和这张表救命。很多设计模式看不出来值不值的时候先想想你当下最痛的问题是什么是if-else越堆越长还是状态流转越来越乱。如果是后者状态模式大概率能帮你从那种改一处崩三处的状态里解放出来。
