资讯详情

告别满屏switch:状态模式让订单状态流转更清晰

📅 2026/9/24 23:15:18 | 华诺云谱 👁 阅读
告别满屏switch:状态模式让订单状态流转更清晰
写代码这些年我见过太多“满屏 switch”的业务类了。尤其是订单、审批、工单这类有明确状态流转的系统核心类里经常是一排排的 switch-case每加一个状态就要动一次老代码。刚开始写起来挺爽的后面一改就炸。今天想和你认真聊聊“状态模式”让对象自己“变身”把散落在各处的状态分支收拢到状态类里。这篇文章适合已经掌握面向对象基础、想提升代码可维护性的开发者我会从问题根源讲起再带着你手写一个完整的订单状态流转例子最后聊聊我实际踩过的坑。1. 满屏 switch 的代码真正的问题是什么1.1 一个典型的“状态处理”场景先还原一下很多项目的真实画风。假设你维护一个订单系统订单状态有这么几个待支付、已支付、已发货、已完成、已取消。每个状态下面都有对应的操作比如待支付状态能支付、能取消已支付状态能发货、能取消已发货状态能确认收货已完成和已取消是终态什么都不让动。很多人的第一版代码长这样public class OrderService { public void pay(Order order) { switch (order.getStatus()) { case PENDING_PAYMENT: // 执行支付逻辑 order.setStatus(OrderStatus.PAID); break; case PAID: throw new IllegalStateException(订单已支付请勿重复支付); case SHIPPED: throw new IllegalStateException(订单已发货不能支付); case COMPLETED: throw new IllegalStateException(订单已完成不能支付); case CANCELLED: throw new IllegalStateException(订单已取消不能支付); default: throw new IllegalStateException(未知状态); } } public void ship(Order order) { switch (order.getStatus()) { case PAID: // 执行发货逻辑 order.setStatus(OrderStatus.SHIPPED); break; case PENDING_PAYMENT: throw new IllegalStateException(订单未支付不能发货); // 其余状态类似 } } public void confirm(Order order) { switch (order.getStatus()) { case SHIPPED: // 执行确认收货逻辑 order.setStatus(OrderStatus.COMPLETED); break; // 其余状态类似 } } public void cancel(Order order) { switch (order.getStatus()) { case PENDING_PAYMENT: case PAID: // 执行取消逻辑 order.setStatus(OrderStatus.CANCELLED); break; // 其余状态类似 } } }这段代码在状态只有两三个、操作只有一两个的时候还好。一旦状态多了、操作多了每个方法里都是一大串 switchOrderService 的代码量肉眼可见地膨胀。想看清楚“已支付状态下到底能做什么”你得把所有方法里的 PAID 分支都翻一遍。1.2 这种写法的四个痛点第一个痛点是可读性差。一个方法里混着五个状态的处理逻辑读代码的人需要在脑子里反复做“状态到行为”的映射。代码超过几百行后别说新人写这段代码的人自己隔一个月回来都费劲。第二个痛点是严重违背开闭原则。每加一个新状态比如“退款中”你得去 pay、ship、confirm、cancel 里各加一个 case。这意味着每次扩展都要改动大量已有代码。改得越多回归测试的范围就越大线上出问题的概率也跟着涨。第三个痛点是分支代码重复。你会发现 cancel 里的“待支付”和“已支付”两个分支处理逻辑经常一模一样只是入口状态的判断不同。很多人图省事直接合并 case虽然跑了但可读性更差了。第四个痛点是测试用例爆炸。每个操作方法都要针对每个状态写用例方法数和状态数的乘积就是测试用例数量的下限。状态越多测试越难写最后大家干脆不测状态流转只测主路径风险全留给了线上。1.3 为什么会写出这样的代码“满屏 switch”不是某个人故意写出来的它通常是一个渐进过程。早期状态很少两三个 if 就能搞定没人愿意为这一点逻辑引入一堆类。后来需求方开始加状态最初定架构的人已经不在团队成员不敢也不愿意去重构核心类只能继续在 switch 里追加 case。等所有人都意识到不对劲的时候这个类已经变成了“谁碰谁背锅”的烫手山芋。这个现象的本质是把“状态”当成了数据的一个普通字段而不是把它当成行为的一个维度。你只是在被动地判断“我现在是什么状态”而不是让状态本身“知道自己该做什么、能去到哪里”。1.4 真正需要解决的问题我们需要的是把每个状态的行为收拢到一起让对象内部的状态“自己决定”对外表现状态之间的迁移规则也由状态自己维护而不是散落在各个业务方法里。这就是状态模式的出发点。2. 状态模式的设计思路让对象自己“变身”2.1 三个核心角色状态模式的定义用一句话说允许对象在其内部状态改变时改变它的行为看起来就像修改了它的类。这里涉及到三个角色Context上下文持有当前状态的对象对外暴露业务方法内部把请求委托给当前状态类。State抽象状态定义一组业务方法的接口所有具体状态类都实现这个接口。ConcreteState具体状态实现 State 接口负责描述“在当前状态下每个业务方法应该怎么表现”同时还可以触发状态切换。对应到订单例子OrderContext 是上下文OrderState 是抽象接口PendingState、PaidState、ShippedState 这些都是具体状态。2.2 把 switch 变成类传统写法里一个方法内部的 switch 是在“以状态为中心”先在当前状态上做多路判断再执行对应逻辑。状态模式的思路是“以状态类为中心”把每个 case 里的逻辑搬进对应的类。Context 持有当前状态调用 pay 时直接执行 state.pay()而不是先判断 state 属于哪个枚举。关键点在于具体状态类里可以持有 Context 的引用。当 pay 这个动作让状态从“待支付”变成“已支付”时状态类内部调用 context.setState(new PaidState()) 就行。状态切换的逻辑跟着状态走放到了最该知道“下一步去哪”的地方。2.3 一个容易听懂的类比你可以想象一个人在不同心情下的表现。心情好的时候同事喊你加班你会笑着答应心情差的时候同样一件事可能直接被怼回去。这里的“心情”就是状态“对加班请求的反应”就是业务方法。如果你用 switch 写每次遇到加班请求都要判断一次当前心情而且心情一变所有判断点都要跟着改。用状态模式的话你只要把“开心状态”和“不开心状态”各自的行为封装好对象自己就知道该做出什么反应。2.4 和策略模式的区别很多人会把状态模式和策略模式搞混。两者看起来都像“把算法/行为放到类里”重点不同维度状态模式策略模式关注点状态之间的迁移、动作触发后状态会改变同一动作下不同算法可替换状态持有状态对象会触发状态切换Context 的状态会变策略对象通常不改变 Context 的身份典型场景订单流转、审批流、工作流排序算法选择、支付渠道选择简单说策略模式解决的是“同一个动作有很多种做法”状态模式解决的是“同一个动作在不同状态下结果不同并且动作可能改变状态”。如果一个对象在调用动作后状态会变那基本就是在状态模式的射程范围内。2.5 状态模式真正带给我们的它并不是“消灭 switch”的银弹。仔细看代码你会发现状态类里面仍然可能存在 if 判断或者很小的 switch。它的核心价值是把一个巨大的分支集合拆成多个小分支让每个小分支归于对应的状态类。这样一来每一个类都只关心一个状态下的所有行为修改时只需要打开相应的类不会误伤其他状态。这就是单一职责和开闭原则在状态场景下的落地。3. 手写一个订单状态模式实战3.1 先定义状态接口用一个接口约束所有状态类必须实现的方法。先看这个接口要暴露哪些操作支付、发货、确认收货、取消。为了让例子完整我让每个方法接收订单上下文和订单对象方法的职责是“校验当前状态下是否允许该操作并执行状态迁移”public interface OrderState { void pay(OrderContext context, Order order); void ship(OrderContext context, Order order); void confirm(OrderContext context, Order order); void cancel(OrderContext context, Order order); }在实际项目中你可能不需要把 Order 也传进去Context 内部可以直接持有 Order。这里为了清晰先把 Order 作为参数传进去避免上下文和订单耦合太深。3.2 实现具体状态类接下来是每个状态一个类。先看“待支付状态”public class PendingPaymentState implements OrderState { Override public void pay(OrderContext context, Order order) { // 执行真正的支付动作 order.pay(); // 切换状态 context.setState(new PaidState()); } Override public void ship(OrderContext context, Order order) { throw new IllegalStateException(订单未支付不能发货); } Override public void confirm(OrderContext context, Order order) { throw new IllegalStateException(订单未支付不能确认收货); } Override public void cancel(OrderContext context, Order order) { order.cancel(); context.setState(new CancelledState()); } }“已支付状态”实现如下注意它能取消也能发货public class PaidState implements OrderState { Override public void pay(OrderContext context, Order order) { throw new IllegalStateException(订单已支付请勿重复支付); } Override public void ship(OrderContext context, Order order) { order.ship(); context.setState(new ShippedState()); } Override public void confirm(OrderContext context, Order order) { throw new IllegalStateException(订单未发货不能确认收货); } Override public void cancel(OrderContext context, Order order) { order.cancel(); context.setState(new CancelledState()); } }“已发货状态”和终态就按规则实现。已发货支持确认收货public class ShippedState implements OrderState { Override public void pay(OrderContext context, Order order) { throw new IllegalStateException(订单已发货不能支付); } Override public void ship(OrderContext context, Order order) { throw new IllegalStateException(订单已发货不能重复发货); } Override public void confirm(OrderContext context, Order order) { order.confirm(); context.setState(new CompletedState()); } Override public void cancel(OrderContext context, Order order) { throw new IllegalStateException(订单已发货不能取消); } }“已完成”和“已取消”是终态所有操作都抛异常。为了节省空间可以用一个抽象基类把默认异常行为集中起来public abstract class TerminalState implements OrderState { Override public void pay(OrderContext context, Order order) { throw new IllegalStateException(订单处于终态不允许支付); } // ship、confirm、cancel 类似 }然后让 CompletedState 和 CancelledState 继承它即可。这里要注意抽象类并不是状态模式的必备部分但它能明显减少重复代码。3.3 定义上下文类上下文类持有当前状态同时把业务方法转发给状态对象public class OrderContext { private OrderState state; public OrderContext() { // 初始状态待支付 this.state new PendingPaymentState(); } public void setState(OrderState state) { this.state state; } public void pay(Order order) { state.pay(this, order); } public void ship(Order order) { state.ship(this, order); } public void confirm(Order order) { state.confirm(this, order); } public void cancel(Order order) { state.cancel(this, order); } }所有业务方法都只有一行调用当前状态对应的方法。后续状态切换由状态类内部完成上下文完全不关心“从哪来到哪去”。3.4 外部调用变成什么样这时候 OrderService 里的代码清爽了public class OrderService { public void pay(Order order) { OrderContext context new OrderContext(); context.pay(order); } public void ship(Order order) { OrderContext context new OrderContext(); context.ship(order); } }当然真实项目里 OrderContext 通常会由当前订单持久化的状态恢复而不是每次 new 一个初始状态。但核心逻辑已经变了业务方法里不再有任何状态分支所有判断都被推给了对应的状态类。3.5 状态流转规则梳理把这个订单示例的状态变迁整理一下待支付支付 → 已支付取消 → 已取消。已支付发货 → 已发货取消 → 已取消。已发货确认收货 → 已完成。已完成终态无流转。已取消终态无流转。状态模式让这些规则分藏在各个状态类里。待支付状态知道“支付后去已支付”已支付状态知道“发货后去已发货”每个状态类只管自己的下一步互不干扰。3.6 代码落地时要注意的细节第一状态对象内部不要存储订单业务数据比如订单金额、收货地址。状态类表达的是“规则”和“迁移”不是“数据”。如果状态类被多个上下文共享里面的可变字段会造成线程安全问题。最好把业务数据放在 Order 里状态类是无状态的或者只持有不变的行为配置。第二状态切换放在动作方法末尾避免状态已经变了但后续逻辑还在执行旧状态的逻辑。我在早期重构时踩过这个坑pay 方法里先调用 order.pay()随后又加了一段“支付成功通知”逻辑结果通知逻辑里读到的状态已经切换导致重复发送事件。把状态切换当作动作的收尾事件来对待会让心智模型更清晰。第三如果状态类不需要被 Spring 管理可以直接 new如果需要注入 Service就要考虑生命周期。后面第 5 部分我会细说。4. 进阶用状态机替换状态类4.1 状态模式的状态迁移分散问题状态模式有一个潜在问题当状态很多时状态迁移规则是散落在各个状态类里的。你想一眼看清“从待支付到已支付有哪些条件”得翻 PendingPaymentState想看清“已支付能不能取消”又得翻 PaidState。如果状态数量超过 10 个这种分散会让维护者觉得头晕。这时候可以换个思路状态机。状态机由“状态”、“事件”、“迁移规则”三要素构成。事件触发后根据当前状态查一张“状态迁移表”看能否迁移到目标状态并执行对应的动作。这种表达方式把规则集中在一张表里非常适合状态多、规则密的场景。4.2 一个轻量级状态机的实现思路不引入第三方框架用枚举和 Map 也能搭一个够用的状态机。核心是定义“事件”和“迁移”两个结构public enum OrderEvent { PAY, SHIP, CONFIRM, CANCEL } public class Transition { final OrderStatus from; final OrderEvent event; final OrderStatus to; public Transition(OrderStatus from, OrderEvent event, OrderStatus to) { this.from from; this.event event; this.to to; } }然后用一个 Map 把“源状态 事件”映射到“目标状态”MapString, OrderStatus transitionTable new HashMap(); transitionTable.put(PENDING_PAYMENT:PAY, OrderStatus.PAID); transitionTable.put(PENDING_PAYMENT:CANCEL, OrderStatus.CANCELLED); transitionTable.put(PAID:SHIP, OrderStatus.SHIPPED); transitionTable.put(PAID:CANCEL, OrderStatus.CANCELLED); transitionTable.put(SHIPPED:CONFIRM, OrderStatus.COMPLETED);需要执行动作时用当前状态和事件拼接出 key查表得到目标状态再执行迁移。如果查不到就说明这个状态下不允许该事件可以抛异常或走兜底逻辑。4.3 两种方案的对比对比维度状态模式状态类拆分状态机迁移表驱动规则集中度分散在各个状态类中集中在一张表中状态行为复杂度每个状态可有复杂行为灵活行为常挂在迁移表上简单动作更好用新增状态需要新增类可能改已有类只需要加迁移表项和状态枚举可读性类多但每个类职责单一表驱动规则一目了然适合场景状态数量少、行为复杂、需要状态内聚状态多、迁移规则多且难以用类表达我个人经验是如果状态只有 3-5 个每个状态下的行为差异明显优先用状态模式代码更符合面向对象直觉如果状态有几十个或者迁移条件错综复杂赶紧上状态机。你甚至可以把状态模式作为状态机里“状态对象”的实现两者不是互斥的。4.4 选型建议实际项目里我也会用更成熟的框架来做状态机比如 Spring StateMachine但它的概念更多学习成本高。如果是中小型项目自研一张迁移表就够了。表驱动的核心是让“规则”可以被快速阅读和修改这比设计模式的“正统”更重要。记住模式是工具不是目的。5. 实战中的常见问题与排查技巧5.1 状态类爆炸了怎么办状态模式最容易被人诟病的就是状态一多类也变多。尤其是订单还可能叠加其他维度比如“支付方式”、“是否允许部分发货”如果把这些维度全部做笛卡尔积状态类数量会爆炸。我建议先拆维度把“状态”和“类型”分开处理类型用策略模式或配置项状态用状态模式。如果还是爆炸直接切到状态机方案。5.2 状态切换后动作重复执行状态类内部如果写得太糙容易出现重复动作。比如支付成功后在 pay 方法里发了一个“支付成功”消息但状态从待支付切到已支付后又因为某些逻辑再次触发了一次 pay导致消息重复。解决办法有几种一是在动作方法开头加状态校验不是期望状态直接抛异常二是给事件加幂等标识三是在状态切换时返回一个“处理结果”对象后续动作只执行一次。5.3 状态模式被滥用的场景并不是所有 switch 都应该换成状态模式。如果状态只有两个比如开启/关闭或者分支逻辑三五年都不会变用状态模式反而会引入大量类增加阅读成本。遇到这种情况一个简单 if 比什么都清晰。判断的标准是“变化频率”和“状态数量”只有当状态迁移规则经常改、状态行为经常扩展时才值得引入状态模式。5.4 排查技巧在 setState 里留一条“状态迁移日志”状态类切换状态时很多人容易漏看“从哪到哪”。我习惯在 OrderContext.setState 里加一行日志或者记录到事件流里public void setState(OrderState newState) { String from this.state.getClass().getSimpleName(); String to newState.getClass().getSimpleName(); // log.info(状态迁移: {} - {}, from, to); this.state newState; }生产环境可以接一个结构化日志排查问题时把某个订单的状态迁移时间轴拉出来一眼就能定位到底是哪个动作把状态改坏了。这个习惯帮我解决过不少疑难 bug强烈推荐。5.5 和 Spring 等框架结合的经验如果状态类需要依赖 Service不能直接在状态类内部 new 出一个状态实例。我一般会把状态类交给 Spring 管理注册成单例 Bean通过 ApplicationContext 获取或者用工厂类组装。注意Spring 单例状态 Bean 必须无状态不能把具体订单数据放在状态 Bean 里。需要操作订单时仍然把订单对象作为方法参数传进去。这种方式既能使用框架的依赖注入能力又能保持状态类的线程安全。还有一个常见坑状态切换时如果上下文被 Spring 管理且有多线程操作状态字段会出现并发问题。这时候建议用有状态的领域对象作为上下文放在方法内不要放到全局单例里。简单说Context 要么每次请求新建要么用 ThreadLocal 保护别把它当成无差别共享的 Bean。最后再分享一个小技巧。我在做状态流转功能时总会先花半天画一张状态迁移表把每个状态允许哪些事件、迁移到哪、有哪些动作写清楚。画完这张表再决定用状态模式还是状态机。很多同事一上来就写代码结果状态漏了迁移规则乱套后面返工成本更高。那种把规则一次性理清的感觉比任何设计模式都管用。状态模式不是万能药但它确实是告别“满屏 switch”的一条好路径。希望你在下次写状态判断判断到手酸的时候能想起让你的对象自己“变身”。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。