1. 状态模式到底是什么从一个被需求反复折磨的场景说起1.1 先聊一个我亲身踩过的坑前几年我负责过一个订单系统最初订单只有几个状态待支付、已支付、已发货、已完成、已取消。一开始我用一个status字段加一堆if-else来判断代码写起来飞快跑得也挺稳。可后来业务同学开始不断加需求——订单要支持退款、退款中、退款失败、部分退款还要支持超时自动关闭、支付后多久没发货自动催单、发货后多久自动确认收货……结果那个判断方法从两百行膨胀到八百多行每改一次逻辑我都要把所有分支重新捋一遍生怕漏掉某个状态组合。最要命的是有次上线后出现已取消状态的订单居然还能被支付排查半天发现是某个if的条件写反了。那次之后我就下决心重构最后用的正是状态模式。整个判断逻辑被拆成一个个独立的类每个状态自己管自己该做什么、能流转到哪代码一下子清爽了后来再加部分退款这种新状态我只新增一个类、改一处配置主流程几乎没动。这篇文章就把我对状态模式的理解、适用场景、优缺点和完整代码示例从头到尾讲清楚。1.2 状态模式的核心三要素状态模式属于行为型设计模式它的核心思想朴素到一句话就能说完把对象在不同状态下的行为抽成独立的类让对象在内部状态改变时它的行为也随之改变。听起来像废话但落到实处它解决了状态一多、条件分支就爆炸这个老大难问题。它的标准结构里通常有三个角色我习惯用订单这个例子来记角色职责订单场景中的对应物Context环境类持有当前状态的引用对外提供统一入口内部把请求委托给当前状态去处理订单对象State抽象状态定义所有状态都要实现的接口通常是各种操作的方法OrderState接口ConcreteState具体状态每个具体状态实现自己的行为并负责状态之间的流转待支付、已支付、已发货……关键在于第三点状态流转的逻辑被放进了具体状态类内部而不是散落在外部一堆if-else里。比如待支付这个状态它自己知道支付之后该变成已支付其余状态完全不用关心这个细节。1.3 为什么说它比看起来更有价值很多人第一次学状态模式会觉得不就是把if-else拆成类吗有必要搞这么复杂我一开始也这么想直到项目里状态真多起来才明白它的价值。if-else的坏处不只是长而是它把三个维度的东西混在了一起什么状态、做什么动作、在什么条件下流转到下一个状态。这三件事在状态很多时会互相干扰改动一个分支很容易影响另一个分支。状态模式做的事情是把它拆开——每个状态只管我是谁、我能做什么、做完去哪职责边界极其清晰。这就是所谓的开闭原则落地新增状态时是扩展而不是修改已有逻辑回归测试的范围因此大大缩小。2. 状态模式适合解决哪些问题适用场景拆解2.1 典型适用场景清单状态模式不是银弹但有几类场景用它是真的香。下面这张表是我这些年总结出来的高匹配度场景你可以对照自己的项目看看场景特征具体例子为什么适合状态模式对象行为随状态明显变化订单、工单、审批流、游戏角色每个状态行为差异大拆类后互不干扰状态数量超过 4~5 个且有流转规则电商订单、物流运单、任务调度判断分支太多维护成本急剧上升状态之间有明显合法/非法约束只有待支付才能支付非法流转逻辑集中在状态内部避免到处判断后续大概率还要加状态业务快速迭代的产品新增状态符合开闭原则状态相关的代码需要被复用或测试核心业务逻辑每个状态类可独立单测我特别想强调的是第四个特征。如果你的状态是死的、永远就那三四种、几乎不会再变那老实说用enum加switch可能更省事。状态模式的收益在变化里才体现得出来。2.2 什么时候不该用状态模式反面例子同样重要。下面这几种情况我建议你就别硬套状态模式了容易把简单问题做复杂状态只有两三个且几乎不变。比如一个开关只有开和关用一个boolean就够了硬拆成两个类纯属自找麻烦。状态之间没有行为差异只是数据标记。如果不同状态只是显示的文字不一样那它更适合当普通字段不需要行为化。状态流转规则是固定的线性流程且不允许扩展。这种直接写死可能更直观。团队对设计模式不熟、项目周期又极短。强行引入模式反而增加沟通和阅读成本。提示判断要不要用状态模式我会问自己两个问题——状态会不会继续变多和每个状态的行为差异大不大。两个都是是基本就可以上有一个是否就要掂量一下。2.3 状态模式和策略模式的区别最容易混面试里被问得最多的就是状态模式和策略模式有啥区别因为这俩结构长得几乎一模一样都是环境类持有接口、接口有多个实现。区别其实不在结构在意图和驱动方式策略模式客户端主动选择一个算法策略策略之间通常互相独立、互不感知选完一般不会自动切换。比如排序选冒泡还是快排是你决定的排序过程中不会自己换。状态模式状态的切换往往是内部驱动的一个状态处理完会自动流转到下一个状态而且状态之间是知道彼此的因为要决定下一个状态是谁。订单支付完自动变已支付这是状态自己在推进。我用一个生活类比策略模式像是你出门前挑交通工具公交、地铁、打车挑定就用状态模式像是你坐飞机通过安检、登机、飞行、降落每个环节结束会自动进入下一个环节是流程在推着你走。理解了这个区别就不会再用错了。3. 状态模式的优缺点别只看教科书上的那几条3.1 优点到底体现在哪里教科书会列一堆优点但我想说说我实际感受到的。第一是消灭了巨型条件分支。还是那个订单例子重构前八百行的方法重构后主流程只剩调用state.handle()可读性的提升不是一点半点。第二是状态相关逻辑高度内聚。待支付的所有规则都在PendingPaymentState里查 bug 时再也不用满项目搜status 1。第三是扩展友好。后来加部分退款状态我新增了一个类加一个流转配置涉及已有代码的改动不到十行回归测试范围极小。第四是便于单测。每个状态类可以单独构造、单独验证流转是否正确测试用例清晰。3.2 缺点和容易被忽视的坑但优点归优点坑也是真坑这些是我踩过才记住的类数量暴涨。状态一多类就多如果项目里还有好几套状态机文件会铺得很开。得配合良好的包结构管理。状态流转被藏起来了。if-else虽然丑但一眼能看全所有分支状态模式把流转分散到各个类里想搞清全局流转需要跳来跳去。我的解法是额外维护一张状态流转表后面讲当作文档和校验依据。增加间接层调试需要多跳一步。断点打在状态类里才能看到流转细节。过度设计风险。前面说过状态少又稳定时用它就是杀鸡用牛刀。3.3 一张表看清取舍维度使用状态模式使用 if-else / switch状态少≤3且稳定过度设计推荐状态多且持续增加推荐维护噩梦状态行为差异大推荐分支臃肿全局流转需要一眼看清需额外维护流转表天然集中但易乱单元测试每个状态可独立测需构造各种状态组合类/文件数量变多少我个人的经验判断是状态数量乘以行为差异度越大越值得上状态模式。4. 手把手代码实现从 Java 到 C4.1 Java 实现订单状态机完整示例先上抽象状态接口定义所有状态都要能响应支付、发货、确认收货、取消这几个动作public interface OrderState { // 支付 void pay(OrderContext context); // 发货 void ship(OrderContext context); // 确认收货 void confirm(OrderContext context); // 取消订单 void cancel(OrderContext context); // 当前状态名方便日志和排查 String name(); }然后是具体状态类。注意每个状态在非法操作时应该明确拒绝而不是默默忽略——这点很关键public class PendingPaymentState 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 name() { return 待支付; } }再写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 name() { return 已支付; } }ShippedState和CancelledState同理前者允许确认收货后者拒绝一切操作。最后是环境类OrderContextpublic class OrderContext { private OrderState state; public OrderContext() { // 初始状态待支付 this.state new PendingPaymentState(); } public void setState(OrderState state) { System.out.println(状态变更 this.state.name() - state.name()); this.state state; } public void pay() { state.pay(this); } public void ship() { state.ship(this); } public void confirm() { state.confirm(this); } public void cancel() { state.cancel(this); } public String currentState() { return state.name(); } }客户端这样用public class Main { public static void main(String[] args) { OrderContext order new OrderContext(); order.pay(); // 待支付 - 已支付 order.ship(); // 已支付 - 已发货 order.confirm(); // 已发货 - 已完成 // 再尝试支付会抛出异常订单已完成不能支付 } }4.2 C 实现要点C 版本结构一样但有内存管理这个绕不开的坎。我给出一个用智能指针的简洁写法避免手动delete出问题#include memory #include stdexcept class OrderContext; class OrderState { public: virtual ~OrderState() default; virtual void pay(OrderContext ctx) 0; virtual void ship(OrderContext ctx) 0; virtual std::string name() const 0; }; class PendingPaymentState : public OrderState { public: void pay(OrderContext ctx) override; void ship(OrderContext ctx) override { throw std::logic_error(待支付订单不能发货); } std::string name() const override { return 待支付; } }; class OrderContext { std::shared_ptrOrderState state; public: OrderContext() : state(std::make_sharedPendingPaymentState()) {} void setState(std::shared_ptrOrderState s) { state std::move(s); } void pay() { state-pay(*this); } void ship() { state-ship(*this); } }; // 状态内部流转 void PendingPaymentState::pay(OrderContext ctx) { // ctx.setState(...) 流转到下一状态 }用shared_ptr管理状态对象一方面省掉了手动释放另一方面如果你把状态对象做成无状态单例所有状态对象共享同一份实例内存开销也能进一步压低。4.3 代码细节里的三个魔鬼第一状态对象的创建位置。我在示例里是每次流转new一个新对象。如果状态对象本身不持有可变数据绝大多数情况如此完全可以把它们做成单例池在Context初始化时把所有状态对象建好流转时只切换引用。这样既省对象创建开销又避免了状态对象内部残留上次的数据。第二非法操作要显式报错。上面每一处throw都是有意的。默默忽略非法操作会制造幽灵 bug——用户点了按钮没反应也不知道为什么。明确抛出异常或返回明确的错误码问题定位成本会低得多。第三null状态处理。Context在初始化时必须给一个合法初始状态别留null。如果实在担心空指针可以在Context里加个默认的空状态对象兜底比到处判空优雅。5. 进阶让状态模式在真实项目里站得住脚5.1 维护一张状态流转表当作唯一真相源这是我强烈推荐的做法。状态分散在各个类里后全局流转就不直观了所以我会额外维护一张表形式通常是一个二维结构当前状态 \ 操作支付发货确认收货取消待支付已支付非法非法已取消已支付非法已发货非法已取消已发货非法非法已完成非法已完成非法非法非法非法已取消非法非法非法非法这张表有两个用途一是当文档新人一眼看懂业务二是当校验依据甚至可以用它反推生成状态机配置减少手写状态类时的漏判。我在项目里就把这张表做成配置配合一个简单的引擎状态类只负责业务动作流转关系由配置驱动进一步降低了出错率。5.2 状态对象要不要持久化订单是要落库的但状态对象本身不需要序列化。落库的永远只是当前状态的名字或编码比如存一个status PAID。读取时根据这个编码从状态池里取回对应的状态对象挂到Context上。这样状态类可以随意重构不影响数据库结构——这一点在设计表结构时想清楚能省很多后患。5.3 并发场景下的小心机高并发下订单状态变更要考虑线程安全。最简单可靠的做法是用数据库乐观锁更新时带上原状态作为条件UPDATE orders SET statusPAID WHERE id? AND statusPENDING_PAYMENT更新影响行数为 0 就说明状态已经被别人改过直接拒绝。这样即使两个人同时点支付也只有一个能成功从根上避免了状态错乱。应用层的状态模式负责逻辑清晰数据库的乐观锁负责并发正确两者分工明确。6. 常见问题与排查技巧实录6.1 常见问题速查表现象可能原因排查/解决思路状态流转后行为没变Context.setState没被调用或调用后没生效在setState里打日志确认引用真的换了加新状态后旧分支报错接口新增方法旧状态类没实现用抽象基类提供默认实现或编译期强制检查并发下状态回退没做锁两个请求互相覆盖引入乐观锁更新时校验原状态状态对象数据串了状态对象被当单例复用却持有可变字段状态对象保持无状态或每次新建全局流转看不清流转逻辑散落各状态类维护状态流转表并纳入评审非法操作无提示状态里对非法操作默默 return改为显式抛异常或返回错误码6.2 三个我踩过的真实坑坑一状态类持有请求级数据。有次我把订单请求参数塞进了状态对象字段结果状态对象被复用后数据串了出现 A 用户的订单显示 B 用户信息的诡异问题。后来彻底改成状态对象只存行为不存数据数据一律通过Context或参数传递问题再没出现。坑二忘了给初始状态赋值。Context构造时忘了设初值第一次调用就空指针。教训是构造方法里必须给出默认状态并在单元测试里覆盖初始状态这个用例。坑三状态流转没有校验合法性。重构时我图省事让每个状态类自己去判断能不能流转结果两个状态类对同一条规则判断不一致。后来统一用 5.1 那张流转表做单一校验点谁也不能绕过去。提示状态模式重构时一定要保证行为等价——重构前后对同一串操作序列的结果必须完全一致。我会拿线上的真实订单日志做回归跑一遍新老代码对比结果这是最靠谱的验证方式。7. 面试和大作业里的高频考点7.1 面试官最爱问的几个问题被问到状态模式翻来覆去就那几个点。第一个是状态模式和策略模式的区别前面讲透了核心是状态内部驱动状态间可知策略外部选择相互独立。第二个是状态模式怎么避免if-else要能说清把流转逻辑下沉到状态类、用多态替代条件判断。第三个是状态多了怎么办答案是状态流转表 配置驱动 池化状态对象。还有一个常见追问是状态模式在哪用到了你可以答订单、工单、审批流、游戏角色比如角色有正常、中毒、眩晕等状态行为和属性都不同、连接状态机等。答得具体比背概念有说服力得多。7.2 大作业怎么做得不像抄的很多设计模式大作业一看就是网上模板改的原因集中在两点用的还是电灯开关那种教科书例子以及只堆了代码没讲清业务。我的建议是选一个你熟悉的真实业务——外卖订单、图书借阅、健身卡核销都行然后重点做三件事一是画清状态流转图二是给每个状态补上真实的业务动作三是加一个非法操作的处理演示。这样即使结构是标准的内容也是你自己的。如果想让作业再出彩一点可以加一个状态流转配置把 5.1 那张表做成 JSON 或 YAML再写一个简单引擎驱动它这就从会用模式升级到会用模式做设计了评分通常能拉开差距。还有个细节容易被忽略日志和可观测性。在setState里统一打一条带从哪到哪、谁触发的日志线上排查状态问题时能救命。我在项目里就靠这条日志把一次偶发的状态错乱定位到了第三方回调重复触发上——如果没日志那种问题能查到你怀疑人生。所以别嫌日志啰嗦状态机这种过程性逻辑日志就是你的眼睛。