订单未支付却调用了发货接口退款单已经关闭了还能被再次审批工单在不同节点之间来回横跳导致下游数据错乱——这些问题追到根上十有八九是同一段上百行的if-else或者switch-case在作祟。状态模式State Pattern就是专门收拾这类烂摊子的设计模式之一它把对象在不同状态下表现不同行为这件事从堆砌的条件语句里抽出来拆成一个个独立的、职责清晰的状态类。这篇博文我会把状态模式的适用场景、优缺点、代码示例、状态迁移设计、并发与持久化的坑结合我在订单和审批流里的实际经验完整讲一遍不管你是正在啃设计模式期末考点还是准备拿它去写大作业或者手上有真实项目要做状态流转应该都能捞到点东西。1. 状态模式到底解决了什么问题1.1 从一个真实的订单状态说起我先不摆教科书定义直接看一段很多项目里都能见到的代码。假设有个订单对象状态有待支付已支付已发货已完成已取消五种状态业务上每种状态下能做的事都不一样待支付可以支付、可以取消已支付可以发货、可以申请退款已发货可以确认收货已完成和已取消基本就是终态了。用最朴素的方式写长这样public class Order { private int status; // 1待支付 2已支付 3已发货 4已完成 5已取消 public void pay() { if (status 1) { // 支付逻辑 status 2; } else if (status 2) { throw new IllegalStateException(订单已支付请勿重复支付); } else if (status 3) { throw new IllegalStateException(订单已发货无法支付); } else if (status 4) { throw new IllegalStateException(订单已完成); } else if (status 5) { throw new IllegalStateException(订单已取消); } } public void ship() { if (status 2) { // 发货逻辑 status 3; } else if (status 1) { throw new IllegalStateException(订单未支付无法发货); } // ... 又是四五个分支 } public void confirm() { // ... 同样的复制粘贴 } public void cancel() { // ... 同样的复制粘贴 } }四个方法每个方法五个分支一共二十个判断而这只是五个状态。如果业务再加一个退款中状态你就得回来把这四个方法全改一遍每个方法里都要塞一个新的else if。更难受的是判断逻辑和业务逻辑混在一起pay()里既有能不能支付的状态校验又有怎么支付的业务动作两者耦合得死死的。这种代码的问题不是不能跑而是每加一个状态、每改一条流转规则修改成本都在非线性增长。圈复杂度上去了单元测试覆盖不过来新人接手看一眼就想跑。状态模式就是针对这个痛点来的把每个状态的行为和它的流转规则各自封装到一个独立的类里让上下文对象把请求委托给当前状态对象去处理。1.2 状态模式的三个核心角色状态模式的结构其实非常干净就三个角色我拿订单这个例子对应着讲。第一个是抽象状态State通常是一个接口或者抽象类它声明了所有状态下都可能有的一系列行为方法。注意这里的关键接口里声明的是这个对象可能被调用到的所有操作而不是每个状态都支持的操作。在订单例子里就是pay()、ship()、confirm()、cancel()这几个方法。第二个是具体状态ConcreteState每一个具体状态类实现抽象状态接口负责两件事一是实现该状态下的真实业务行为二是决定并执行接下来转到哪个状态。比如PendingPaymentState的pay()方法里干的是执行支付逻辑然后把上下文的当前状态切换成PaidState。第三个是上下文Context它是外部世界唯一能看到的东西持有一个当前状态对象的引用对外提供的方法基本上就是转发给当前状态对象。外部调用order.pay()的时候订单对象不知道也不关心现在是什么状态它只是把pay()转给currentState.pay(this)由当前状态自己判断该做什么、能不能做、做完转到哪里。把这套结构画成职责图大概是这样的分工角色职责订单例子中的对应Context持有当前状态、对外暴露统一入口、转发请求、保存状态无关的公共数据Order 类持有订单号、金额、当前状态对象State定义所有可能被请求的操作契约OrderState 接口ConcreteState实现本状态下的行为与状态转移PendingPaymentState、PaidState 等状态转移的方向是外部驱动、内部决策调用的触发来自外部但能不能转、转到哪这个决策被放进了具体状态类内部。这一点是状态模式最核心的设计取舍也直接决定了它的优缺点后面我会详细展开。1.3 它和状态机不是一回事但经常一起用很多人第一次接触状态模式会把它和有限状态机FSM混为一谈。严格说状态模式是一种代码组织方式解决的是如何把状态相关行为从条件分支里解放出来而状态机是一种建模方式解决的是如何定义一套严谨的状态、事件、迁移规则。前者是面向对象的实现手段后者更接近形式化描述。实际项目里两者经常搭着用先用状态机或者一张状态迁移表把规则的草图定下来再用状态模式落地成代码。比如订单的状态迁移表可以写成这样当前状态事件目标状态是否允许待支付支付已支付是待支付取消已取消是待支付发货-否已支付发货已发货是已支付取消已取消是已发货确认收货已完成是已完成任意-否已取消任意-否有了这张表状态模式里每个具体状态类该实现什么、该拒绝什么就一目了然了。我个人的习惯是规则简单状态少于五个、迁移不太绕就直接上状态模式状态多、迁移复杂、还需要支持可视化配置或者动态调整的就引入成熟的状态机框架做底座。这个取舍点很关键因为状态模式最遭人诟病的地方就是状态一多类的数量会膨胀得厉害。2. 什么场景适合用状态模式2.1 典型适用场景清单状态模式不是万能钥匙但有几类场景一旦遇上用它几乎是条件反射。我把这几年踩过坑的几类整理出来。第一类对象行为强依赖自身状态且状态会在运行时改变。这是状态模式的教科书场景。订单、工单、审批单、支付单、游戏里的角色行为、TCP 连接的建立与关闭、播放器的播放暂停停止都属于这一类。判断标准很简单如果你发现同一个方法名在不同状态下要执行完全不同的逻辑而且这些逻辑还会随着状态变化切换那就是它了。第二类代码里存在又长又臭的状态相关条件语句。具体一点一个方法里出现了三层以上嵌套的if-else或者一个switch-case里塞了十几个分支且这些分支的入口条件是某个状态字段——只要命中这个特征就可以考虑重构。我一般用圈复杂度工具跑一遍方法复杂度超过 15 就重点看。第三类状态流转规则需要被明确表达和约束。有些业务的合规要求很高比如金融审批你必须能明确回答这个状态下允许哪些操作以及这个操作执行后一定到哪个状态。把这些规则散在一堆条件分支里评审和测试都很难覆盖全。状态模式把每条规则放到对应状态类里规则本身就是代码可读性和可测性都会好很多。第四类需要独立测试每个状态的逻辑。条件分支堆在一起的代码想单独测试已发货状态下确认收货这条路径就得构造一个状态为已发货的对象再调方法很多时候还得搭一堆前置数据。而状态模式下每个具体状态类都能单独实例化、单独测试依赖的上下文对象也可以用 mock 或者轻量实现替代测试成本低很多。2.2 这些场景就别硬套状态模式反过来讲有几类情况我见过有人生搬硬套结果得不偿失。状态少且几乎不变的情况。如果只有两个状态比如启用/禁用迁移规则就是来回切换写一个if或者用布尔字段就够了。为两个状态建一整套接口加两个实现类纯属过度设计。我见过一个配置项开关用了状态模式代码量翻了好几倍维护的人叫苦不迭。状态只影响数据、不影响行为的场景。有些状态本质上只是个展示标签比如订单的来源渠道不同渠道下方法执行逻辑完全一样只是返回值里带个渠道名。这种用枚举字段就够了状态模式的核心价值在于行为随状态变化行为不变就没有意义。状态之间的迁移是无条件线性推进的场景。比如一个流程固定就是 A→B→C→D没有分支、没有回退、没有并行那用一个简单的步骤计数器或者流程引擎配置就够了不需要为每个节点写一个类。团队对模式不熟、代码要交给别人长期维护的情况。这个是我在实践中特别想强调的。状态模式带来的抽象层级是有成本的如果有人接手你的代码却不知道这些态类的分工逻辑他可能连为什么支付要先找到对应状态类都要想半天。所以在用之前最好确认这是团队能接住的东西或者至少把状态迁移图画清楚放进文档。2.3 和策略模式、简单工厂的边界在哪这是设计模式面试里出现频率极高的问题也是很多人写代码时真正会犹豫的地方。状态模式和策略模式的结构几乎一模一样都是 Context 抽象接口 若干实现但意图完全不同这是本质区别。策略模式的策略之间是平行、互不感知的由外部客户端决定用哪个策略策略之间不会互相切换。比如支付方式选择微信还是支付宝决定权在用户手里微信策略不需要知道支付宝策略的存在。而状态模式的状态之间是有依赖、会互相流转的状态的切换往往由内部逻辑驱动一个状态知道自己下一步可能变成什么。这就是为什么状态模式的实现类通常要持有上下文的引用——因为要能调用ctx.setState(...)。再看和简单工厂的关系。简单工厂解决的是对象创建的问题它不管行为切换。实际项目里状态模式经常会组合简单工厂来用上下文初始化和状态对象获取时用一个工厂或者一个 Map 来按枚举值取对应的状态实例避免到处new。这是很实用的组合拳我后面给代码的时候会加上。对比维度状态模式策略模式简单工厂核心意图行为随状态变化状态间自动流转同一行为的多套算法可替换集中创建对象状态/策略间关系有依赖互相感知平行互不感知不涉及切换驱动方内部逻辑驱动为主外部客户端指定调用方传参典型落地订单、工单、审批流计费算法、排序算法、支付渠道对象初始化把这张表记住基本就不会在选型上犯迷糊了。我见过有人用策略模式硬做订单状态流转结果每个策略里都要写一堆if去判断当前状态来源绕了一圈又回到原点。3. 优缺点拆解与工程取舍3.1 它带来的四个实质好处第一个好处干掉庞大的条件分支。这是最直观的。前面那二十个判断用状态模式之后会分散到五个状态类里每个类只关心自己的一两个行为。代码从一个大方法里横向铺开变成多个小类里纵向收敛可读性是数量级的提升。第二个好处状态转移显式化。在条件分支写法里支付成功后状态变成已支付这句话藏在status 2这行赋值里数字 2 是什么含义要靠注释或者常量表去查。而在状态模式里这行变成ctx.setState(new PaidState())或者ctx.setState(PaidState.INSTANCE)转移到哪里一目了然。第三个好处符合开闭原则。增加一个新状态主要工作是新增一个状态类再在相关的旧状态类里补充什么事件下会流转到新状态的触发点。虽然不能做到完全零修改毕竟流转关系在那里摆着但至少不需要去改一大堆已有的业务方法体。第四个好处单一职责和可测性。每个状态类的职责是明确的单元测试可以对着状态类逐个击破。我在实际项目里状态类的测试覆盖率通常能做到 90% 以上因为它们的依赖很干净。3.2 它的三个真实代价优点讲完了必须讲讲代价否则就是耍流氓。状态模式最被低估的问题不是类变多而是下面这几个。代价一状态类数量膨胀。五个状态就是五个类十个状态就是十个类如果每种状态还有多个行为类的数量会很可观。状态一多整个包里的文件会显得很碎。应对办法有两个一是状态对象做成无状态的单例用枚举实现最省事后面给代码二是如果状态确实多到十几个以上考虑用状态表 反射或者状态机框架替代。代价二状态迁移逻辑分散全局视角容易丢失。这是最隐蔽、在真实维护中最容易出问题的一点。每个状态类只知道自己能转到哪不知道别人能不能转过来。当你想回答哪些状态可以到达已完成这个问题时你没法在一个地方看到答案得把所有状态类翻一遍。我的应对方式是强制要求有状态迁移表文档或者代码里的迁移配置和实现类保持同步更新。这个习惯帮我避免过好几次线上事故。代价三可能违反迪米特法则和引入循环依赖。状态类要调用上下文的setState()同时上下文要持有状态对象——两者互相引用。有的团队会因此觉得设计不干净。这个其实不算大问题但在包结构设计上要注意把状态接口和上下文放同一层具体状态放子包避免跨模块乱引用。结合起来看我的经验判断是状态数量在 3 到 8 个之间、迁移规则相对集中、团队有面向对象基础是状态模式的最佳甜点区。低于三个不值得高于八个要考虑换方案。3.3 用枚举实现状态类能省掉一半的类很多人不知道在 Java 里用枚举实现状态模式是一个特别优雅的变体能极大缓解类膨胀的问题。枚举天生就是单例每个枚举常量可以有自己的方法实现等于把状态类折叠进了枚举常量里。对于状态不多、每个状态行为又不复杂的场景这个方案的代码量比传统状态模式少一半以上。我在几个中小型项目里用过效果很好。具体的写法我会在第 4 章给出完整示例。优点是对比明显的省类文件、天然线程安全、状态集合集中在一处便于总览。缺点也很清楚——枚举常量里塞太多逻辑会让单个文件变得很长破坏可读性所以它更适合每个状态行为比较轻的场景。这算是状态模式的一种轻量落地变形值得掌握。4. 从零实现订单状态流转的完整代码4.1 设计上下文和抽象状态我先用最标准的对象式写法把订单例子完整实现一遍然后再给枚举变体。首先是抽象状态接口它声明订单可能被调用的所有操作public interface OrderState { /** 支付 */ void pay(OrderContext ctx); /** 发货 */ void ship(OrderContext ctx); /** 确认收货 */ void confirm(OrderContext ctx); /** 取消订单 */ void cancel(OrderContext ctx); }注意我把OrderContext作为参数传进每个方法这是状态模式的关键技巧状态对象本身不持有上下文而是通过参数获取这样状态类就能保持无状态可以安全地做成单例被所有订单共享。如果让状态类持有上下文引用那状态对象就和具体订单绑定了不能被复用内存开销会明显上升。这一点是我踩过坑之后总结的很多教材上的实现会直接在构造里传上下文那个做法在订单量大的系统里很要命。上下文类负责保存订单本身的业务数据订单号、金额等以及当前状态对外暴露的四个方法全部转发给当前状态public class OrderContext { private final String orderNo; private final BigDecimal amount; private OrderState currentState; public OrderContext(String orderNo, BigDecimal amount) { this.orderNo orderNo; this.amount amount; // 新订单默认是待支付状态 this.currentState PendingPaymentState.INSTANCE; } 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 void setState(OrderState state) { System.out.println(订单 orderNo 状态流转: currentState.getClass().getSimpleName() - state.getClass().getSimpleName()); this.currentState state; } public String getOrderNo() { return orderNo; } public BigDecimal getAmount() { return amount; } public OrderState getCurrentState() { return currentState; } }这里的setState里我特意加了状态流转日志这个习惯很重要。状态模式最大的隐患就是流转路径分散没日志的话排查线上问题时根本不知道订单是怎么走到现在这个状态的。日志里带上订单号、原状态、目标状态出问题的时候一查就知道。4.2 五个具体状态类的实现细节接下来是五个状态类。我这里用INSTANCE单例字段来保证状态对象无状态可复用。public class PendingPaymentState implements OrderState { public static final PendingPaymentState INSTANCE new PendingPaymentState(); Override public void pay(OrderContext ctx) { System.out.println(订单 ctx.getOrderNo() 支付成功金额 ctx.getAmount()); ctx.setState(PaidState.INSTANCE); } Override public void ship(OrderContext ctx) { throw new IllegalStateException(订单未支付不能发货); } Override public void confirm(OrderContext ctx) { throw new IllegalStateException(订单未支付不能确认收货); } Override public void cancel(OrderContext ctx) { System.out.println(订单 ctx.getOrderNo() 已取消); ctx.setState(CancelledState.INSTANCE); } }待支付状态下的逻辑很清楚能支付、能取消不能发货、不能确认。一到非法操作就抛异常把规则约束住。public class PaidState implements OrderState { public static final PaidState INSTANCE new PaidState(); Override public void pay(OrderContext ctx) { throw new IllegalStateException(订单已支付请勿重复支付); } Override public void ship(OrderContext ctx) { System.out.println(订单 ctx.getOrderNo() 已发货); ctx.setState(ShippedState.INSTANCE); } Override public void confirm(OrderContext ctx) { throw new IllegalStateException(订单未发货不能确认收货); } Override public void cancel(OrderContext ctx) { System.out.println(订单 ctx.getOrderNo() 已取消触发退款); ctx.setState(CancelledState.INSTANCE); } }已支付状态下能发货、能取消取消会触发退款不能重复支付也不能确认收货。public class ShippedState implements OrderState { public static final ShippedState INSTANCE new ShippedState(); Override public void pay(OrderContext ctx) { throw new IllegalStateException(订单已发货不能支付); } Override public void ship(OrderContext ctx) { throw new IllegalStateException(订单已发货请勿重复发货); } Override public void confirm(OrderContext ctx) { System.out.println(订单 ctx.getOrderNo() 已确认收货); ctx.setState(CompletedState.INSTANCE); } Override public void cancel(OrderContext ctx) { throw new IllegalStateException(订单已发货不能直接取消请走退货流程); } }已发货状态只能确认收货其他操作一律拒绝。注意这里不能直接取消的提示语我特意加了引导因为实际业务里已发货的订单确实要走退货流程而不是直接取消这种提示对使用方很有帮助。public class CompletedState implements OrderState { public static final CompletedState INSTANCE new CompletedState(); Override public void pay(OrderContext ctx) { throw new IllegalStateException(订单已完成不能操作); } // ship / confirm / cancel 同理全部抛出已完成提示 }public class CancelledState implements OrderState { public static final CancelledState INSTANCE new CancelledState(); Override public void pay(OrderContext ctx) { throw new IllegalStateException(订单已取消不能操作); } // 其余方法同理 }完成态和取消态是终态四个方法都只做拒绝。这里有个细节值得聊非法操作是抛异常还是静默忽略我的经验是对于订单这类强一致性业务非法操作应该快速失败抛异常让调用方知道状态不对。但如果是对外的接口直接抛异常可能不友好通常会在上层捕获后转换成业务错误码返回。这个分层处理很重要异常是内部信号错误码才是给用户的。4.3 客户端调用与执行效果组装起来用一下感受状态流转的过程public class Client { public static void main(String[] args) { OrderContext order new OrderContext(NO20240101, new BigDecimal(199.00)); order.pay(); // 待支付 - 已支付 order.ship(); // 已支付 - 已发货 order.confirm(); // 已发货 - 已完成 try { order.cancel(); // 已完成非法操作 } catch (IllegalStateException e) { System.out.println(操作被拒绝: e.getMessage()); } } }打印结果大致是这样订单 NO20240101 状态流转: PendingPaymentState - PaidState 订单 NO20240101 支付成功金额 199.00 订单 NO20240101 状态流转: PaidState - ShippedState 订单 NO20240101 已发货 订单 NO20240101 状态流转: ShippedState - CompletedState 订单 NO20240101 已确认收货 操作被拒绝: 订单已完成不能操作可以看到整条链路走下来客户端代码里没有一个if-else状态判断和流转全部藏在状态对象内部。这就是状态模式最舒服的地方——调用方只管调状态该怎么变是对象自己的事。4.4 用枚举实现的轻量变体现在给你看枚举版本。同样是订单用枚举实现的话状态类变成枚举常量每个常量重写方法public enum OrderStateEnum { PENDING_PAYMENT { Override public void pay(OrderContext ctx) { System.out.println(支付成功); ctx.setState(PAID); } Override public void ship(OrderContext ctx) { throw new IllegalStateException(未支付不能发货); } Override public void cancel(OrderContext ctx) { ctx.setState(CANCELLED); } }, PAID { Override public void pay(OrderContext ctx) { throw new IllegalStateException(已支付不能重复支付); } Override public void ship(OrderContext ctx) { System.out.println(发货成功); ctx.setState(SHIPPED); } Override public void cancel(OrderContext ctx) { ctx.setState(CANCELLED); } }, SHIPPED { Override public void pay(OrderContext ctx) { throw new IllegalStateException(已发货不能支付); } Override public void ship(OrderContext ctx) { throw new IllegalStateException(已发货不能重复发货); } Override public void cancel(OrderContext ctx) { throw new IllegalStateException(已发货请走退货流程); } }, COMPLETED { /* 全部拒绝 */ }, CANCELLED { /* 全部拒绝 */ }; public abstract void pay(OrderContext ctx); public abstract void ship(OrderContext ctx); public abstract void cancel(OrderContext ctx); }枚举版本的优点很突出所有状态集中在一个文件里状态集合一目了然天然单例、线程安全代码量比传统版本少三分之一到一半。但它的使用边界也很明确——每个状态的行为逻辑要足够轻。如果你的状态方法里要注入一堆服务、要查数据库、要发消息那塞进枚举会非常别扭还是老老实实用独立类通过构造函数注入依赖。我一般是这样判断的状态方法里只有状态流转和少量日志、通知就用枚举状态方法里涉及复杂业务编排和依赖注入就用传统对象式。4.5 状态获取用工厂或者 Map别到处 new最后补一个工程化的小优化。如果不用单例常量而是每次需要状态对象时new一个既不环保也不方便统一管理。我通常会在上下文里维护一个从状态标识到状态对象的映射public class OrderContext { private static final MapString, OrderState STATE_CACHE new HashMap(); static { STATE_CACHE.put(PENDING, PendingPaymentState.INSTANCE); STATE_CACHE.put(PAID, PaidState.INSTANCE); STATE_CACHE.put(SHIPPED, ShippedState.INSTANCE); STATE_CACHE.put(COMPLETED, CompletedState.INSTANCE); STATE_CACHE.put(CANCELLED, CancelledState.INSTANCE); } /** 从数据库恢复订单时用状态码还原状态对象 */ public void restoreState(String stateCode) { OrderState state STATE_CACHE.get(stateCode); if (state null) { throw new IllegalArgumentException(未知状态码: stateCode); } this.currentState state; } }这个restoreState是持久化场景的必备技能。数据库里存的是状态码字符串订单恢复时得把状态码映射回状态对象。我见过有人用状态类的getClass().getName()存数据库然后反射还原实话说这种做法很不稳一旦类名重构或者包名调整历史数据就废了。用显式的状态码映射简单可靠是我强烈推荐的方式。5. 常见问题与排查技巧实录5.1 高频问题速查表状态模式用起来坑不算多但有几个问题反复出现。我把它们整理成表方便对照排查。现象常见原因解决思路状态加了但某个操作没生效新状态遗漏了某个方法的重写接口方法全量检查或者接口用抽象类统一给默认拒绝实现状态对象被多个订单共享后数据错乱状态类持有上下文引用且有成员变量状态类必须无状态上下文通过方法参数传入非法操作没有拦住反而继续往下执行具体状态里的拒绝逻辑没写或者写成空方法每个状态类明确写非法操作的处理不要默认放行恢复订单后状态不对持久化的状态码和映射不一致用集中映射表禁止用类名做持久化标识并发下状态被覆盖、重复转账状态判断和更新不是原子的数据库带状态条件的乐观锁更新或加分布式锁状态迁移路径理不清迁移分散在各状态类缺少全局视图维护状态迁移表代码和文档同步维护5.2 并发下的状态一致性是我见过最多的事故源这个问题必须在博文里重点讲因为它太容易翻车了。状态模式本身是单机内存层面的模式它不负责并发安全。当两个请求同时打到同一个订单上——比如用户连点两次支付按钮或者前端超时重试——内存里pay()的判断和setState()之间如果有时间差就完全可能出现重复支付或者状态被覆盖。单机场景下最直接的解法是给上下文的关键操作加锁比如把pay()之类的方法整体synchronized或者用ReentrantLock。但即使单机加锁在分布式部署下也没用——两个请求可能落到不同的机器上各自的内存状态互不可见。这种场景下内存里的状态对象其实只是一个缓存视图真正的权威状态在数据库里。我的做法是状态流转用数据库带状态条件的更新语句来兜底。比如支付成功后把订单状态从待支付更新为已支付SQL 写成UPDATE orders SET status PAID WHERE order_no ? AND status PENDING_PAYMENT判断影响行数如果为 0 说明状态已经被别人改过了本次操作失败。这叫乐观锁是处理状态并发最靠谱的手段。内存里的状态模式负责组织代码数据库的条件更新负责保证一致性两者分工明确。这个配合方式在订单、支付、审批这类系统里几乎是标配我很建议你在写状态流转代码时就把这层考虑进去。5.3 状态爆炸了怎么办状态一多状态模式的日子就难过了。我见过最夸张的一个业务状态有二十多个迁移规则上百条如果硬用状态模式就是二十多个类加上每类要重写十几个方法代码简直没法看。遇到这种情况我一般有两条路。第一条是拆分聚合根。二十多个状态往往不是一个维度的比如订单状态支付状态物流状态其实是三条独立的线强行揉成一个状态枚举当然爆炸。把它们拆成独立的子状态各自用状态模式复杂度立刻就降下来了。第二条路是上状态机框架。Spring StateMachine、Cola StateMachine 这类框架专门解决状态多、迁移复杂的场景它们用配置或者注解定义迁移规则天然支持状态迁移的持久化和事件驱动。当然框架有学习成本只有当状态模式确实扛不住的时候才考虑别一上来就过度设计。5.4 几个我踩过的实操心得再说几个不太会写在文档里、但实际开发中很有用的细节。心得一状态流转日志一定要打而且要对齐格式。我统一用订单号 原状态 目标状态 触发事件 时间戳这个格式日志平台按订单号聚合查看排查问题时能直接看到一条完整的状态流转链路。没有这个日志线上出状态问题基本就是盲猜。心得二把状态迁移写成表让实现和表对得上。我在每个用到状态模式的项目里都会维护一份状态迁移表最好能用单元测试遍历这张表,逐个验证每条合法迁移能通过、每个非法操作会被拒绝。这种表驱动测试能覆盖绝大多数状态问题投入产出比很高。心得三非法操作的处理要分级。内部调用非法操作抛IllegalStateException让调用方感知对外接口层捕获后转成统一业务错误码而像重复支付这种可能因为重试导致的幂等场景其实更适合返回操作成功或者静默处理而不是报错。分级处理能避免很多不必要的错误告警。心得四状态类不做重活只做分流。状态类的职责应该是判断能不能做 决定下一步状态真正的业务逻辑比如真实的支付、发货尽量下沉到领域服务里去调用状态类只做编排。这样状态类保持轻量好测试业务逻辑也能被其他场景复用。6. 最后聊聊怎么真正把状态模式用顺手写到这里状态模式的原理、场景、优缺点、代码、坑基本上都过了一遍。我想说的是状态模式真正的门槛不在结构而在于你能不能先想清楚状态迁移这张图。我见过太多人一上来就写状态类写到最后发现流转关系理不顺返工重来。正确的顺序应该是先把状态、事件、迁移规则列清楚最好画成表确认没有漏洞和死循环再动手写代码。代码只是这张图的翻译。另外也别把状态模式当银弹。它适合行为随状态变化的场景不适合所有和状态沾边的场景。三个状态以下、只影响数据展示、迁移无条件线性推进这些情况用枚举或者普通字段就够了。我自己的经验值是状态在 3 到 8 个、有非法操作需要拦截、需要独立的流转日志满足这三条状态模式基本就是最优解超出了就该考虑状态机框架或者拆分子状态了。如果你是在准备设计模式的大作业或者期末考建议至少手写一遍订单或者电梯的例子把上下文、抽象状态、具体状态三个角色的职责写清楚能画得出类图和状态迁移图基本就能拿不错的分数。如果是真实项目那就在实现之前先把迁移表外置成配置或者文档配合带状态条件的数据库更新保证一致性这套组合我在多个项目里验证过稳。