当前位置: 首页 > news >正文

设计模式 18 · 状态模式

前面我们埋过好几次钩子:策略那篇说"策略是平行选一个,状态是链式流转",这一篇终于轮到状态模式(State)正式登场,把这条界限彻底讲透。它和策略模式的代码结构几乎一模一样(都是持有一个接口引用、委托调用),却是行为型里最经典的一对"双胞胎"——长得像,内核完全不同。

状态模式解决的问题一句话:一个对象的行为,随它自身所处的"状态"而变化,而且状态之间会按规则相互流转。关键词是"行为随状态变"和"状态会流转"。生活里的例子随处可见:一部电梯,在"运行中"你按开门键没反应,在"停靠中"按了才开门——同一个动作(按开门),在不同状态下行为完全不同;而电梯的状态还会流转:停靠→运行→停靠。红绿灯也是:红灯亮着,时间到了自动变绿,绿变黄,黄变红,状态按固定规则一环扣一环地转。

我们的订单场景里,有一个教科书级的状态机例子:订单的生命周期。一笔订单会经历"待付款 → 已付款 → 已发货 → 已完成"这一串状态,中间还可能"取消"。而订单的行为强烈依赖于它当前的状态:"待付款"的订单可以支付、可以取消;"已发货"的订单不能取消、只能确认收货;“已完成"的订单什么操作都不能做。同样是"取消"这个动作,在不同状态下,有的允许、有的直接拒绝。如果用一堆if-else判断当前状态来决定行为,代码会变成一个又臭又长、状态一多就失控的泥潭。状态模式,就是把每个状态做成一个独立的类,让状态自己决定"在我这个状态下,各个动作该怎么做、该流转到哪个状态”。

这篇文章按这条线索展开:先看"用状态字段 + 一堆 if-else 判断"的困境;再引出状态模式如何把每个状态对象化;然后讲清它的角色、以及状态流转的两种驱动方式;接着重点辨析状态与策略这对双胞胎(这是本篇的核心);最后给出适用边界。贯穿例是订单状态机。

目录

  1. 用 if-else 判断状态的困境
  2. 状态模式:把每个状态对象化
  3. 角色,与状态流转谁来驱动
  4. 状态 vs 策略:一对双胞胎的彻底辨析
  5. 什么时候用状态模式

一、用 if-else 判断状态的困境

看订单的操作。订单有个"当前状态"字段,每个操作都要先判断当前状态、再决定怎么做:

publicclassOrder{privateStringstatus;// "待付款"、"已付款"、"已发货"、"已完成"、"已取消"// 支付操作publicvoidpay(){if(status.equals("待付款")){status="已付款";// 只有待付款能支付}else{thrownewRuntimeException("当前状态不能支付");}}// 取消操作publicvoidcancel(){if(status.equals("待付款")){status="已取消";// 待付款能取消}elseif(status.equals("已付款")){status="已取消";// 已付款也能取消(要退款)}else{thrownewRuntimeException("当前状态不能取消");// 已发货/已完成不能取消}}// 发货操作publicvoidship(){if(status.equals("已付款")){status="已发货";}else{thrownewRuntimeException("当前状态不能发货");}}// ... 每个操作都是一坨状态判断}

这段代码能跑,但随着状态和操作变多,它会迅速失控:

  • 每个方法都是一坨 if-else:每个操作(pay/cancel/ship/confirm…)内部,都要把所有相关状态判断一遍。状态一多,每个方法都又臭又长。
  • 状态转移规则散落各处:"待付款能干什么、能转到哪些状态"这个规则,被打散在 pay、cancel、ship 各个方法里。你想搞清楚"待付款"状态的完整行为,得把所有方法翻一遍。
  • 违反开闭原则:新增一个状态(比如"退款中"),你得回到每一个操作方法里,都加一个else if分支。改一处,全都要动、全都要重测。
  • 容易出非法流转:全靠人肉判断,一不小心就可能让订单从"已完成"又跳回"待付款"这种非法状态,而编译器毫不知情。

问题的根源是:"状态"只是一个字符串字段,而"每个状态下的行为和转移规则"被硬编码、打散在各个操作方法的 if-else 里。我们真正想要的是:把每一个状态都变成一个独立的对象,让这个状态对象自己封装"在我这个状态下,各个操作该怎么响应、该流转到哪个状态"。这样,"待付款"的所有规则都集中在待付款状态这一个类里,清清爽爽。这就是状态模式。

二、状态模式:把每个状态对象化

状态模式的做法:定义一个状态接口,声明所有操作;每个具体状态是一个类,实现"在本状态下这些操作各自怎么做、流转到哪个状态";订单(上下文)持有一个"当前状态"对象,把操作委托给它,并允许状态切换。

第一步,定义状态接口(声明所有操作):

publicinterfaceOrderState{voidpay(OrderContextctx);voidcancel(OrderContextctx);voidship(OrderContextctx);// ... 所有操作}

第二步,每个状态是一个类,封装本状态下的行为和转移:

// 待付款状态publicclassPendingPayStateimplementsOrderState{publicvoidpay(OrderContextctx){System.out.println("支付成功");ctx.setState(newPaidState());// 流转到"已付款"}publicvoidcancel(OrderContextctx){System.out.println("订单已取消");ctx.setState(newCancelledState());// 流转到"已取消"}publicvoidship(OrderContextctx){thrownewRuntimeException("未付款不能发货");// 本状态不允许}}// 已付款状态publicclassPaidStateimplementsOrderState{publicvoidpay(OrderContextctx){thrownewRuntimeException("请勿重复支付");}publicvoidcancel(OrderContextctx){System.out.println("已付款订单取消,发起退款");ctx.setState(newCancelledState());}publicvoidship(OrderContextctx){System.out.println("已发货");ctx.setState(newShippedState());// 流转到"已发货"}}// ShippedState、CompletedState、CancelledState 同理...

第三步,上下文(订单)持有当前状态,把操作委托出去:

publicclassOrderContext{privateOrderStatestate=newPendingPayState();// 初始状态:待付款publicvoidsetState(OrderStatestate){this.state=state;}// 所有操作,都委托给"当前状态"对象去处理publicvoidpay(){state.pay(this);}publicvoidcancel(){state.cancel(this);}publicvoidship(){state.ship(this);}}

用起来,订单的行为随状态自动变化,流转由状态自己驱动:

OrderContextorder=newOrderContext();// 待付款order.pay();// 支付成功 → 自动流转到已付款order.ship();// 已发货 → 自动流转到已发货order.cancel();// 抛异常:已发货不能取消

对比第一节,升级点非常清晰:每个状态的所有规则(本状态下各操作怎么响应、转到哪)都集中在它自己的类里,一个if-else都没有;OrderContext只管把操作委托给当前状态;要加一个"退款中"状态,新增一个RefundingState类就行,已有状态类基本不用动。"订单当前能干什么"这件事,由"当前是哪个状态对象"来决定,而不是靠满地的状态判断。用一张图看这个"状态机"最清楚:

图里最该记住的,是那张状态之间带箭头的流转图——它就是业务上的"订单状态机"。状态模式的精髓,就是把这张状态图,从散落的 if-else 里,变成一组各自独立的状态类 + 它们之间明确的流转关系。哪些流转是合法的,一目了然地写在各状态类里;非法的流转(比如已完成跳回待付款)压根没有对应代码,自然就杜绝了。

三、角色,与状态流转谁来驱动

状态模式的角色,三个:

角色本例中是谁职责
上下文(Context)OrderContext持有当前状态,把操作委托给它
抽象状态(State)OrderState接口声明各状态共有的操作
具体状态(ConcreteState)PendingPayState封装本状态的行为 + 流转逻辑

一个关键的设计问题:状态的流转(从一个状态切到下一个),该由谁来驱动?有两种做法:

  • 由状态对象自己驱动(我们上面用的):每个状态在处理操作时,自己决定"处理完该转到哪个状态",调用ctx.setState(...)完成切换。好处是流转逻辑高度内聚——"待付款之后去哪"这个规则,就写在PendingPayState里。缺点是状态类之间产生了依赖(PendingPayState得知道PaidState的存在)。
  • 由上下文统一驱动:状态对象只处理行为、返回结果,由上下文根据结果决定切到哪个状态。好处是状态之间解耦,流转规则集中在一处。缺点是上下文又变重了一点。

实际项目里两种都有。"状态自己驱动"更符合状态模式的原味(把状态机分散进各状态),适合流转规则相对固定的场景;"上下文驱动"适合流转规则复杂、需要集中管理的场景。对于订单这种流转清晰的状态机,状态自驱通常更自然。

四、状态 vs 策略:一对双胞胎的彻底辨析

这是本篇的重头戏。状态和策略,是设计模式里最像的一对——把它们的类图画出来,几乎完全一样:都有一个 Context 持有一个接口引用,都把行为委托给具体实现,都用"多态"替代了"条件分支"。很多人到这里就懵了:它们到底有什么区别?

区别不在结构,而在意图和用法,有三个关键差异:

差异一:各个"实现"之间有没有关系。

  • 策略的各个算法是完全平行、互相独立的。运费的"包邮策略"和"按重策略"之间,毫无关系,谁也不知道谁的存在。
  • 状态的各个状态是互相关联、会流转的。“待付款"知道自己之后会变成"已付款”,“已付款"知道自己会变成"已发货”——状态之间有明确的转移关系,构成一张状态图。

差异二:谁来决定用哪个"实现"。

  • 策略:由外部(客户端)决定用哪个策略,主动setStrategy(new WeightShipping())塞进去。策略自己不会切换自己。
  • 状态:通常由状态自己(或上下文)根据业务规则自动流转到下一个状态。你不会从外面"设置"订单是什么状态,而是订单在操作过程中自己转过去。

差异三:切换的频率和意图。

  • 策略:一旦选定,通常在整个过程中不变(这一单就用按重计费)。切换是"换一种做法"。
  • 状态:在对象生命周期里不断流转(订单从待付款一路变到完成)。切换是"进入了下一个阶段"。

用一句话彻底记住:策略是"横向地、平行地选一个算法"(选择),状态是"纵向地、按规则流转的一串阶段"(流转)。或者更形象:策略像是"点菜"——菜单上的菜互不相干,你挑一个;状态像是"闯关"——一关通了自动进下一关,关卡之间有顺序。结构骗人,意图不骗人。

还有一个实用的判断技巧:看有没有"状态转移图"。如果你能为这些"实现"画出一张"从 A 状态到 B 状态"的箭头图,那就是状态模式;如果这些"实现"之间画不出任何箭头、纯粹是平级的选项,那就是策略模式。订单能画出状态机图 → 状态;运费的几种算法画不出流转 → 策略。

五、什么时候用状态模式

适合用状态模式的信号:

  • 一个对象的行为明显依赖于它的状态,不同状态下同一操作的行为差异很大;
  • 存在明确的状态流转规则(能画出状态机图);
  • 代码里出现了大量"根据状态字段做 if-else/switch"的判断,且状态和操作都会增加。

不必用的信号:

  • 状态就两三个、行为差异也不大——那简单的 if-else 或枚举更直白,套状态模式凭空多出一堆状态类,是过度设计;
  • 各"状态"之间没有流转关系,只是平级选项——那是策略,不是状态;
  • 状态机极其复杂(几十个状态、上百条流转规则)——那可能需要专门的状态机引擎/工作流框架(如 Spring StateMachine),手写状态类会难以维护。

判断的核心还是那句话:先确认真的存在"行为随状态变 + 状态按规则流转"这个结构(能画出状态机图),状态模式才值得上。而且要和策略分清楚——别把一堆平行算法硬说成"状态"。


小结。状态模式把一个对象的每个状态都变成一个独立的状态类,让状态自己封装"在本状态下各操作怎么响应、该流转到哪个状态",上下文只持有当前状态对象并委托操作——从而把散落在各方法里的状态 if-else,变成一组清晰的状态类 + 明确的流转关系(状态机),新增状态不动或少动已有代码,还天然杜绝非法流转。它和策略是结构几乎相同的双胞胎,但策略是平行地选一个算法、由外部指定、通常不变;状态是按规则流转的一串阶段、自动切换、不断变化——能画出状态转移图的就是状态。订单生命周期是它最经典的应用。下一篇我们讲命令模式——它把"一个请求/操作"本身封装成对象,从而能把操作排队、记录、撤销和重做,典型如订单操作的"撤销"功能。

http://www.jsqmd.com/news/1369156/

相关文章:

  • 03-像素坐标转机械臂世界坐标原理详解
  • 常州靠谱的装修短视频代运营公司怎么选?龙宸节点(常州运营中心) - 热点品牌推荐
  • 圆柱式滴灌管生产线设备定制厂家如何选择 新润滴灌设备 - 热点品牌推荐
  • 2026年不锈钢渔网刀源头厂家哪家靠谱?认准东台市久天机械制造有限公司 - 热点品牌推荐
  • 2026年安徽合肥工业及民用门类定制安装厂家选型参考:电动卷帘门,pvc快速门,电动伸缩门,防火门,工业提升门 - 海棠依旧大
  • 2026年广州医疗家具厂家推荐:医院病房、药房、护士站、手术室家具选型指南 - 海棠依旧大
  • 2026年全国环流熏蒸优质工厂推荐 河南粮安智能设备有限公司 - 起跑123
  • 2026年苏州高新区企业财税代办怎么选?本地化全流程财税服务适配中小微企业需求 - 海棠依旧大
  • 2026年开封二手滚筒筛沙机选购指南与本地服务解析 - 热点品牌推荐
  • 2026年获取正规刨花机厂家联系电话实用指南 - 热点品牌推荐
  • 04-手眼标定理论:眼在手上、眼在手外适用场景
  • 戴南有实力的波纹管换热管生产厂商对接江苏巨登不锈钢管业有限公司(戴南销售部) - 热点品牌推荐
  • 2026年泰州制造业工厂短视频运营服务细致的机构盘点 - 起跑123
  • 如何选择可靠的烤炉点火针供应商?2026年专业指南 - 热点品牌推荐
  • 2026年寻410钢丝制造商联系方式?实测详解安徽耐润新材料科技有限公司 - 热点品牌推荐
  • 2026 年新发布:北镇靠谱的短视频运营平台哪家专业,为啥有人靠这玩意儿年入百万,你做却连饭钱都赚不到? - 行业严选官
  • 2026 年现阶段,成都靠谱的回收电缆品牌哪个好,收废品的李叔靠这玩意儿,今年多赚了小二十万? - 企业推荐官【认证】
  • 2026 年 8 月新发布:西双版纳球场围栏 源头厂家/设备围栏 厂家联系电话,你天天用的办公设备,居然还能藏着这么个护安全的好工具?-迈鹏丝网 - 领域鉴赏官
  • 2026年全国优质环流熏蒸工厂推荐 河南粮安智能设备 - 起跑123
  • 05-手眼标定完整实操:棋盘格标定+参数求解
  • S7-1200 PLC装箱程序1(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_
  • 国内气动冲切水口机源头工厂推荐怎么选深圳市壹零壹精密设备有限公司 - 热点品牌推荐
  • 高可用组件如何做分片:规划、分配与故障接管
  • Claude Code自动模式实战:安全边界、配置避坑与效率提升指南
  • 矿用干式变压器厂商推荐几家?结合2026年实际看金山门科技有限公司 - 热点品牌推荐
  • 2026 年新消息:新罗口碑好的彩色双层保护膜定制厂家有哪些,别再贴普通膜了!这款双层的居然这么好用? - 企业推荐官-
  • 客户真实口碑调研:2026 重庆鸿善诚天岚 PC 供应靠谱吗? - 天下观知
  • 2026年大连数控机床公司推荐:大连数控设备、大连车床加工、大连机床定制、大连数控装备、大连机械加工设备优选指南 - 海棠依旧大
  • 2026年广州医疗家具厂家推荐:医院病房、药房、护士站、手术室家具及医用家具选型指南 - 海棠依旧大
  • 2026年周口二手球磨机设备回收优选指南与洛阳途福废旧物资回收有限公司(周口服务中心) - 热点品牌推荐