news 2026/9/7 8:08:14

CS-Notes 状态模式精讲:State Pattern 状态转移原理与糖果销售机 Java 实现全解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CS-Notes 状态模式精讲:State Pattern 状态转移原理与糖果销售机 Java 实现全解

CS-Notes 状态模式精讲:State Pattern 状态转移原理与糖果销售机 Java 实现全解

【免费下载链接】CS-Notes:books: 技术面试必备基础知识、Leetcode、计算机操作系统、计算机网络、系统设计项目地址: https://gitcode.com/GitHub_Trending/cs/CS-Notes

本文基于 CS-Notes 仓库中 设计模式 - 状态 一文展开,系统讲解行为型设计模式之一——状态模式(State Pattern)的核心意图、类图结构与实现细节。文章以经典的糖果销售机(Gumball Machine)为实战载体,逐行拆解State接口、四个具体状态类、上下文类GumballMachine及客户端调用代码,并结合运行输出还原完整的状态转移过程。读完后你不仅能照着仓库代码写出可运行的状态模式示例,还能理解"对象看起来像修改了它所属的类"这一描述的底层机制,以及它与策略模式的分野。

一、状态模式要解决的问题(Intent)

状态模式的定义非常精炼:

允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它所属的类。

这句话包含两层含义:

  1. 行为随内部状态变化:一个对象对外提供的操作结果,取决于它当前所处的内部状态。例如同一台糖果机,"投入 25 分钱"在未投币、已投币、售罄三种状态下,行为完全不同。
  2. "像修改了类":由于所有行为都被委托给当前状态对象去执行,当状态对象被切换时,从调用方视角看,这个对象仿佛运行时换了一个"类型",行为整体发生了改变——但实际上类的身份从未变化,只是内部持有的状态对象变了。

如果不使用状态模式,这类逻辑通常表现为一个类内部大量的if / else ifswitch分支:每个动作方法都要判断当前状态再决定行为,状态越多分支越膨胀,且新增状态需要改动所有方法,极易出错。状态模式的解法是:把每一种状态封装成一个独立的类,每个类只负责"在该状态下如何响应各个动作",从而将条件分支转化为多态分发。

在 CS-Notes 的设计模式体系中,状态模式属于行为型模式,与策略模式同为第 8/9 个讲解条目,二者常被放在一起比较(详见后文第六节)。

二、模式结构:类图与参与者职责

状态模式的通用类图如下:

从类图可以看到三个核心角色:

角色类图元素职责
Context(上下文)持有state属性与request()方法维护一个"当前状态"引用,request()把请求委托给当前状态对象处理;对外提供状态切换入口(通常是一个setState()方法)
State(状态接口/抽象类)抽象handle()方法定义所有具体状态共有的动作契约,每一种动作对应一个方法
ConcreteState(具体状态)ConcreteStateAConcreteStateB各自实现接口,封装"处于该状态时对每个动作的响应",并可触发上下文的状态切换

关键设计点是:Context 自己不实现任何状态相关业务,它只是一个"状态容器 + 委托转发器"。请求进来后被转交给state.handle(),真正的逻辑在具体状态类里。状态类在被处理动作的过程中,往往需要驱动 Context 切换到下一个状态,因此具体状态类通常会持有 Context 的引用。

三、实战建模:糖果销售机与它的四种状态

3.1 业务场景

糖果销售机是一个理解状态模式最经典的例子:它内部有多种状态,每种状态下对外呈现不同的行为,且状态之间可以发生转移,转移的同时销售机的行为也随之改变。

它的工作过程大致是:投入 25 分钱 → 转动曲柄 → 掉出一颗糖果 → 回到初始状态;若糖果耗尽则进入售罄状态。围绕这个过程,销售机一共有4 种状态

状态含义处于该状态时可接受的合法动作
No Quarter(未投币)默认等待投币投币 → 进入 Has Quarter
Has Quarter(已投币)已投入 25 分钱退币 → 回到 No Quarter;转动曲柄 → 进入 Sold
Sold(售出中)正在发放糖果发放动作完成后回 No Quarter 或进 Sold Out
Sold Out(售罄)库存为 0无法投币、退币、转动

所有动作(事件)一共有 4 个,与状态一一组合后,就构成了销售机的完整行为矩阵。

3.2 状态转移图

上图直观展示了状态转移的全貌:

  • 投币(inserts quarter):No Quarter → Has Quarter;
  • 退币(ejects quarter):Has Quarter → No Quarter;
  • 转动曲柄(turns crank):Has Quarter → Sold;
  • 发放糖果(dispense gumball):Sold → No Quarter(前提是库存仍大于 0);
  • 库存归零:进入 Out of Gumballs。

对照上图和后面的代码可以发现,图中箭头对应的正是代码里各状态类调用gumballMachine.setState(...)完成的跳转,二者一一对应。

四、Java 实现逐模块拆解

本仓库的 状态模式原文 给出了完整可运行的实现(见原文第 17~301 行),下面按模块展开并补充机制分析。

4.1 状态接口 State

public interface State { /** * 投入 25 分钱 */ void insertQuarter(); /** * 退回 25 分钱 */ void ejectQuarter(); /** * 转动曲柄 */ void turnCrank(); /** * 发放糖果 */ void dispense(); }

接口把销售机的 4 类"事件"抽象成 4 个方法。关键点在于:每个具体状态类都必须为全部 4 个事件提供响应——即使某个事件在当前状态非法(例如没投币就转动曲柄),也要给出明确的拒绝行为而不是"方法不存在"。这正是把状态机中"非法动作"也纳入建模的方式,让每个状态的行为完全自洽。

4.2 具体状态一:NoQuarterState(未投币)

public class NoQuarterState implements State { GumballMachine gumballMachine; public NoQuarterState(GumballMachine gumballMachine) { this.gumballMachine = gumballMachine; } @Override public void insertQuarter() { System.out.println("You insert a quarter"); gumballMachine.setState(gumballMachine.getHasQuarterState()); } @Override public void ejectQuarter() { System.out.println("You haven't insert a quarter"); } @Override public void turnCrank() { System.out.println("You turned, but there's no quarter"); } @Override public void dispense() { System.out.println("You need to pay first"); } }

分析:

  • 该状态是销售机的默认可交易状态。只有insertQuarter()是合法动作:打印提示后调用gumballMachine.setState(getHasQuarterState())完成转移;
  • 其余三个动作(退币、转曲柄、发放)都是非法的,仅输出提示,不改变状态;
  • 注意字段gumballMachine由构造器注入——每个状态类都持有了"当前这台机器"的引用,因此才能触发状态跳转。这也是状态模式中"谁负责转移"的一种实现选择:由具体状态对象来驱动 Context 切换状态

4.3 具体状态二:HasQuarterState(已投币)

public class HasQuarterState implements State { private GumballMachine gumballMachine; public HasQuarterState(GumballMachine gumballMachine) { this.gumballMachine = gumballMachine; } @Override public void insertQuarter() { System.out.println("You can't insert another quarter"); } @Override public void ejectQuarter() { System.out.println("Quarter returned"); gumballMachine.setState(gumballMachine.getNoQuarterState()); } @Override public void turnCrank() { System.out.println("You turned..."); gumballMachine.setState(gumballMachine.getSoldState()); } @Override public void dispense() { System.out.println("No gumball dispensed"); } }

分析:

  • 重复投币被拒绝insertQuarter()输出 "You can't insert another quarter",保证一枚硬币只能触发一次交易;
  • ejectQuarter()是"反悔"入口:退币后回到NoQuarterState
  • turnCrank()是正常交易路径:转到SoldState,真正的发糖逻辑交给 Sold 状态完成;
  • dispense()非法:已经投了币但还没转曲柄,机器不会提前吐糖。

4.4 具体状态三:SoldState(售出中)

public class SoldState implements State { GumballMachine gumballMachine; public SoldState(GumballMachine gumballMachine) { this.gumballMachine = gumballMachine; } @Override public void insertQuarter() { System.out.println("Please wait, we're already giving you a gumball"); } @Override public void ejectQuarter() { System.out.println("Sorry, you already turned the crank"); } @Override public void turnCrank() { System.out.println("Turning twice doesn't get you another gumball!"); } @Override public void dispense() { gumballMachine.releaseBall(); if (gumballMachine.getCount() > 0) { gumballMachine.setState(gumballMachine.getNoQuarterState()); } else { System.out.println("Oops, out of gumballs"); gumballMachine.setState(gumballMachine.getSoldOutState()); } } }

分析——这一段是整个示例最核心的"售货结算逻辑":

  • dispense()真正执行发糖:先调用gumballMachine.releaseBall()让库存减一;
  • 然后根据剩余库存决定下一状态getCount() > 0说明还有货,回到NoQuarterState等待下一位顾客;否则打印 "Oops, out of gumballs" 并转入SoldOutState
  • 其余三个动作全部拒绝:turnCrank()输出 "Turning twice doesn't get you another gumball!",从状态层面杜绝了"转动两次曲柄骗取两颗糖"的漏洞。

从这里可以看到状态模式处理条件转移的优雅之处:发完糖后去哪,是由"当前状态对象 + 运行时库存条件"共同决定的,Context 完全不需要感知这些分支。

4.5 具体状态四:SoldOutState(售罄)

public class SoldOutState implements State { GumballMachine gumballMachine; public SoldOutState(GumballMachine gumballMachine) { this.gumballMachine = gumballMachine; } @Override public void insertQuarter() { System.out.println("You can't insert a quarter, the machine is sold out"); } @Override public void ejectQuarter() { System.out.println("You can't eject, you haven't inserted a quarter yet"); } @Override public void dispense() { System.out.println("No gumball dispensed"); } @Override public void turnCrank() { System.out.println("You turned, but there are no gumballs"); } }

当库存归零后,机器进入这个"终态":4 个事件全部输出拒绝信息,任何操作都不会再改变状态(若需要"补货"逻辑,通常可在此状态中增加相应方法把机器带回NoQuarterState,本示例未涉及)。

4.6 上下文类 GumballMachine(状态容器 + 委托转发)

public class GumballMachine { private State soldOutState; private State noQuarterState; private State hasQuarterState; private State soldState; private State state; private int count = 0; public GumballMachine(int numberGumballs) { count = numberGumballs; soldOutState = new SoldOutState(this); noQuarterState = new NoQuarterState(this); hasQuarterState = new HasQuarterState(this); soldState = new SoldState(this); if (numberGumballs > 0) { state = noQuarterState; } else { state = soldOutState; } } public void insertQuarter() { state.insertQuarter(); } public void ejectQuarter() { state.ejectQuarter(); } public void turnCrank() { state.turnCrank(); state.dispense(); } public void setState(State state) { this.state = state; } public void releaseBall() { System.out.println("A gumball comes rolling out the slot..."); if (count != 0) { count -= 1; } } public State getSoldOutState() { return soldOutState; } public State getNoQuarterState() { return noQuarterState; } public State getHasQuarterState() { return hasQuarterState; } public State getSoldState() { return soldState; } public int getCount() { return count; } }

GumballMachine是状态模式的 Context,需要重点理解它的四个设计:

1. 构造期一次性创建全部状态并共享

构造器里把四个状态对象全部new出来并各保存一份,同时把当前机器this注入进去。这样做的好处是:四个状态对象在整个生命周期内复用,状态切换只是更换state指针的指向,不会反复创建对象。这种"共享状态对象 + 共享 Context"的结构是状态模式实现的常见形态。

2. 初始状态由库存决定

if (numberGumballs > 0) { state = noQuarterState; } else { state = soldOutState; }

刚出厂的机器如果一颗糖都没有,直接进入售罄状态;否则进入未投币状态等待交易。这种"构造条件决定初始状态"的写法非常实用。

3. 对外动作方法全部是"裸转发"

insertQuarter()ejectQuarter()等只是调用state对应的方法,没有任何业务逻辑。唯一的例外是turnCrank()

public void turnCrank() { state.turnCrank(); state.dispense(); }

它把"转曲柄"和"发糖"串在一起连发两次调用。这里的顺序极其微妙:第一次state.turnCrank()执行时,state还是HasQuarterState,该方法内部已经把 Context 切到SoldState;紧接着第二次调用state.dispense()时,state字段已经被更新为 SoldState,于是真正执行的是SoldState.dispense(),这才触发了releaseBall()发糖。若把两行顺序颠倒——先dispense()turnCrank()——HasQuarterState.dispense()只会打印 "No gumball dispensed",糖就永远吐不出来。理解"Context 在两次连续委托之间重新读取了自己可变的 state 字段",是读懂这段代码的关键。

4. Context 为状态对象提供"转移工具"

setState()getSoldOutState() / getNoQuarterState() / getHasQuarterState() / getSoldState()这组方法,本质上是状态对象用来完成跳转的公共接口;releaseBall()getCount()则暴露了库存的读写能力。整个状态机的数据(库存)集中在 Context,而规则(何时跳转到哪)分散在各状态类中,两者通过方法调用互相配合。

4.7 客户端驱动与完整运行结果

public class Client { public static void main(String[] args) { GumballMachine gumballMachine = new GumballMachine(5); gumballMachine.insertQuarter(); gumballMachine.turnCrank(); gumballMachine.insertQuarter(); gumballMachine.ejectQuarter(); gumballMachine.turnCrank(); gumballMachine.insertQuarter(); gumballMachine.turnCrank(); gumballMachine.insertQuarter(); gumballMachine.turnCrank(); gumballMachine.ejectQuarter(); gumballMachine.insertQuarter(); gumballMachine.insertQuarter(); gumballMachine.turnCrank(); gumballMachine.insertQuarter(); gumballMachine.turnCrank(); gumballMachine.insertQuarter(); gumballMachine.turnCrank(); } }

用 5 颗糖初始化机器后,连续进行了一组"买糖—退币—非法操作—直到售罄"的完整流程,运行输出如下:

You insert a quarter You turned... A gumball comes rolling out the slot... You insert a quarter Quarter returned You turned, but there's no quarter You need to pay first You insert a quarter You turned... A gumball comes rolling out the slot... You insert a quarter You turned... A gumball comes rolling out the slot... You haven't insert a quarter You insert a quarter You can't insert another quarter You turned... A gumball comes rolling out the slot... You insert a quarter You turned... A gumball comes rolling out the slot... Oops, out of gumballs You can't insert a quarter, the machine is sold out You turned, but there are no gumballs No gumball dispensed

对照输出可以完整还原一遍状态轨迹(初始 count = 5,状态 = No Quarter):

步骤调用状态轨迹与输出要点
1投币 + 转曲柄No Quarter → Has Quarter → Sold → 发糖,count 4 → No Quarter
2投币 → 退币Has Quarter → No Quarter("Quarter returned")
3直接转曲柄停留在 No Quarter:"You turned, but there's no quarter" + "You need to pay first"
4投币 + 转曲柄正常售出一颗,count 3 → No Quarter
5投币 + 转曲柄正常售出一颗,count 2 → No Quarter
6退币当前为 No Quarter:"You haven't insert a quarter"
7连续投币第一次进入 Has Quarter,第二次被拒:"You can't insert another quarter"
8转曲柄售出一颗,count 1 → No Quarter
9投币 + 转曲柄售出最后一颗,count 0:"Oops, out of gumballs" → Sold Out
10投币 / 转曲柄全部被拒,机器停留在 Sold Out

整段输出恰好印证了状态模式的两个承诺:同一动作在不同状态下的行为不同(如第 3 步与第 4 步同为turnCrank(),结果却一个是拒绝、一个是出货);状态转移由内部条件自动推进(如发糖后根据count决定回 No Quarter 还是进 Sold Out)。

五、从实现反推机制:为什么"像修改了类"

结合上面的代码,可以提炼出状态模式的四条底层机制:

  1. 行为 = 对当前 State 的多态调用。Context 对外的方法体只有一行委托,真正决定"做什么、给什么反馈"的是运行时state指针指向的具体状态类。切换状态指针,就等于整体更换了行为集合,调用方无感知——这便是"看起来像修改了类"的本质。

  2. 状态对象持有 Context 引用以驱动转移。四个状态类都在构造时注入gumballMachine,并通过setState(...)切换指针。这种"状态自己决定后继状态"的风格让每个状态类既是"行为载体"又是"转移规则表",不需要 Context 维护复杂的转移矩阵。

  3. 状态对象可共享、可复用。状态对象自身基本不保存可变业务数据(唯一的库存count存放在 Context),因此机器在构造期一次性创建四个状态对象并反复切换使用即可,内存开销恒定。

  4. 状态机对非法动作是"显式建模"而非"运行时防御"。每个非法组合都对应一行拒绝输出,任何动作在任何状态下都有确定的行为结果,程序不会出现"未定义状态"。

六、状态模式与策略模式的异同(仓库姊妹篇)

状态模式的类图与策略模式高度相似,二者都通过组合(Context 持有接口引用)实现运行时动态改变对象行为。仓库的策略模式文档中专门设有"与状态模式的比较"一节(见原文第 16~20 行),观点如下:

状态模式的类图和策略模式类似,并且都是能够动态改变对象的行为。但是状态模式是通过状态转移来改变 Context 所组合的 State 对象,而策略模式是通过 Context 本身的决策来改变组合的 Strategy 对象。……状态模式主要是用来解决状态转移的问题……策略模式主要是用来封装一组可以互相替代的算法族。

二者的差异可以总结为下表:

维度状态模式(State)策略模式(Strategy)
关注点解决"状态转移"问题,状态改变驱动行为改变封装可互换的算法族,客户端按需替换算法
谁来决定切换内部状态类根据运行条件自行触发转移(通常无需客户端干预)Context / 客户端主动调用setStrategy()指定算法
切换时机必须在运行过程中由内部条件触发任意时刻可替换,属客户端决策
行为集合每个状态必须完整实现接口的全部动作(含拒绝动作)每个策略通常只关心自己那一个算法的实现
典型例子糖果销售机、TCP 连接状态机鸭子叫(Quack/Squeak)、排序算法替换

判定一个场景该用谁,可以这样问:这些对象是"客观存在的不同状态、且会自然迁移"(用 State),还是"同一件事的多种等价做法、由使用者挑选"(用 Strategy)?前者关心状态机正确流转,后者关心算法解耦替换。

七、适用场景与工程权衡

7.1 适合使用状态模式的特征

  • 对象的行为强烈依赖其内部状态,且同一事件在不同状态下行为差异很大;
  • 代码里存在大量针对"状态"的条件判断(if (state == XX)),并且新增状态需要改动多处;
  • 状态数量有限、可枚举,且状态之间的转移关系相对明确、需要集中管理。

现实中的典型场景包括:订单状态机、TCP 连接状态、审批流、游戏角色状态(待机/移动/攻击)、工作流引擎等。

7.2 状态模式带来的收益

  • 消除巨型条件分支:状态判断被多态分发替代,每个方法只剩一行委托;
  • 符合开闭原则:新增状态只需新增一个状态类并在转移处接入,不必修改已有状态类;
  • 单一职责:每种状态的行为内聚在独立类中,便于单独阅读与测试;
  • 转移规则显式化:所有可能的状态组合与动作响应都被显式建模,运行时不会出现"悬空"分支。

7.3 需要警惕的代价

  • 类数量膨胀:状态较多时类文件激增,需配合包结构与命名规范管理;
  • 转移逻辑分散:每个状态内部各自调用setState(),全局转移路径不再集中,复杂状态机中较难一眼看清全貌;
  • 上下文状态暴露:Context 通常要暴露setState()与各状态对象的 getter(本示例的GumballMachine即如此),封装性有所削弱。

在实践中若状态与转移比较简单,也可考虑用枚举 + 单一类的简化状态机替代完整的状态模式;只有状态行为足够复杂、相互差异足够大时,把它拆成独立类才划算。这是工程上需要权衡的部分,原文档与仓库设计模式目录中并未对此展开,这里仅作实现层面的补充讨论。

八、仓库阅读路径与参考资料

如需在 CS-Notes 仓库中继续深入,推荐按以下顺序阅读:

  • 状态模式原文(本主题的完整代码与输出):本文所有代码的直接出处,含第 17~301 行完整实现;
  • 策略模式文档:与状态模式配套学习,其中的"与状态模式的比较"一节是理解二者差异的关键;
  • 设计模式目录:状态模式位于"行为型"分类下,便于查看其在 23 种经典模式中的位置;
  • 设计模式.md:仓库内全部设计模式条目的汇总文件,适合整体通读。

据设计模式目录列出的参考资料,本主题内容主要参考自弗里曼《Head First 设计模式》(中国电力出版社,2007)与 Gamma 等《设计模式:可复用面向对象软件的基础》(机械工业出版社,2007)等经典著作,糖果销售机示例是贯穿这些教材讲解状态模式的标准案例,仓库代码对其做了完整、可直接运行的中文注释版复现。

综上,状态模式通过"把状态封装成类、把行为变成对当前状态对象的多态委托",用组合与多态化解了状态机中最棘手的条件分支问题。掌握糖果销售机这个最小可运行示例,就等于拿到了状态模式从类图到状态转移、再到工程取舍的完整知识闭环。

【免费下载链接】CS-Notes:books: 技术面试必备基础知识、Leetcode、计算机操作系统、计算机网络、系统设计项目地址: https://gitcode.com/GitHub_Trending/cs/CS-Notes

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 8:07:32

Unity 6,Unity 2022和团结引擎到底怎么选

背景Unity中国下架了unity6,并且在官网删除了unity6相关的信息。同时unity付费证书分为国内版本和国际版本,以及团结引擎。团结引擎是unity中国基于unity2022针对国内定制的游戏引擎,目前主要是增加了平台支持。官方表示会在后续逐步添加unity6的相关功能…

作者头像 李华
网站建设 2026/9/7 8:04:38

用Delphi自研一维条形码控件:从Code128原理到扫码枪实战

简介:这是一份面向Delphi 6开发者的条形码控件源码,目标是在不安装外部插件的前提下,为老版本IDE项目补充一维条形码生成能力。控件支持Code 39与Code 128等常见制式,前者可编码数字、大写字母与部分符号,后者则能覆盖…

作者头像 李华
网站建设 2026/9/7 8:02:25

C#上位机调用VisionPro实战:ToolBlock加载、扫码触发与九点标定

简介:一份面向C#机器视觉开发者的源码示例,演示如何调用VisionPro库实现图像圆形检测。方案基于Cognex.VisionPro_dotNET,通过CogFindCircleTool工具完成读取图像、设定半径范围、执行查找并显示结果,适用于制造质检、尺寸测量等自…

作者头像 李华