ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

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状态模式的定义非常精炼允许对象在内部状态改变时改变它的行为对象看起来好像修改了它所属的类。这句话包含两层含义行为随内部状态变化一个对象对外提供的操作结果取决于它当前所处的内部状态。例如同一台糖果机投入 25 分钱在未投币、已投币、售罄三种状态下行为完全不同。像修改了类由于所有行为都被委托给当前状态对象去执行当状态对象被切换时从调用方视角看这个对象仿佛运行时换了一个类型行为整体发生了改变——但实际上类的身份从未变化只是内部持有的状态对象变了。如果不使用状态模式这类逻辑通常表现为一个类内部大量的if / else if或switch分支每个动作方法都要判断当前状态再决定行为状态越多分支越膨胀且新增状态需要改动所有方法极易出错。状态模式的解法是把每一种状态封装成一个独立的类每个类只负责在该状态下如何响应各个动作从而将条件分支转化为多态分发。在 CS-Notes 的设计模式体系中状态模式属于行为型模式与策略模式同为第 8/9 个讲解条目二者常被放在一起比较详见后文第六节。二、模式结构类图与参与者职责状态模式的通用类图如下从类图可以看到三个核心角色角色类图元素职责Context上下文持有state属性与request()方法维护一个当前状态引用request()把请求委托给当前状态对象处理对外提供状态切换入口通常是一个setState()方法State状态接口/抽象类抽象handle()方法定义所有具体状态共有的动作契约每一种动作对应一个方法ConcreteState具体状态ConcreteStateA、ConcreteStateB各自实现接口封装处于该状态时对每个动作的响应并可触发上下文的状态切换关键设计点是Context 自己不实现任何状态相关业务它只是一个状态容器 委托转发器。请求进来后被转交给state.handle()真正的逻辑在具体状态类里。状态类在被处理动作的过程中往往需要驱动 Context 切换到下一个状态因此具体状态类通常会持有 Context 的引用。三、实战建模糖果销售机与它的四种状态3.1 业务场景糖果销售机是一个理解状态模式最经典的例子它内部有多种状态每种状态下对外呈现不同的行为且状态之间可以发生转移转移的同时销售机的行为也随之改变。它的工作过程大致是投入 25 分钱 → 转动曲柄 → 掉出一颗糖果 → 回到初始状态若糖果耗尽则进入售罄状态。围绕这个过程销售机一共有4 种状态状态含义处于该状态时可接受的合法动作No Quarter未投币默认等待投币投币 → 进入 Has QuarterHas Quarter已投币已投入 25 分钱退币 → 回到 No Quarter转动曲柄 → 进入 SoldSold售出中正在发放糖果发放动作完成后回 No Quarter 或进 Sold OutSold Out售罄库存为 0无法投币、退币、转动所有动作事件一共有 4 个与状态一一组合后就构成了销售机的完整行为矩阵。3.2 状态转移图上图直观展示了状态转移的全貌投币inserts quarterNo Quarter → Has Quarter退币ejects quarterHas Quarter → No Quarter转动曲柄turns crankHas Quarter → Sold发放糖果dispense gumballSold → No Quarter前提是库存仍大于 0库存归零进入 Out of Gumballs。对照上图和后面的代码可以发现图中箭头对应的正是代码里各状态类调用gumballMachine.setState(...)完成的跳转二者一一对应。四、Java 实现逐模块拆解本仓库的 状态模式原文 给出了完整可运行的实现见原文第 17~301 行下面按模块展开并补充机制分析。4.1 状态接口 Statepublic 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 havent insert a quarter); } Override public void turnCrank() { System.out.println(You turned, but theres 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 cant 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 cant insert another quarter保证一枚硬币只能触发一次交易ejectQuarter()是反悔入口退币后回到NoQuarterStateturnCrank()是正常交易路径转到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, were 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 doesnt 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 doesnt 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 cant insert a quarter, the machine is sold out); } Override public void ejectQuarter() { System.out.println(You cant eject, you havent 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 theres 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 havent insert a quarter You insert a quarter You cant 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 cant 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 Quarter2投币 → 退币Has Quarter → No QuarterQuarter returned3直接转曲柄停留在 No QuarterYou turned, but theres no quarter You need to pay first4投币 转曲柄正常售出一颗count 3 → No Quarter5投币 转曲柄正常售出一颗count 2 → No Quarter6退币当前为 No QuarterYou havent insert a quarter7连续投币第一次进入 Has Quarter第二次被拒You cant insert another quarter8转曲柄售出一颗count 1 → No Quarter9投币 转曲柄售出最后一颗count 0Oops, out of gumballs → Sold Out10投币 / 转曲柄全部被拒机器停留在 Sold Out整段输出恰好印证了状态模式的两个承诺同一动作在不同状态下的行为不同如第 3 步与第 4 步同为turnCrank()结果却一个是拒绝、一个是出货状态转移由内部条件自动推进如发糖后根据count决定回 No Quarter 还是进 Sold Out。五、从实现反推机制为什么像修改了类结合上面的代码可以提炼出状态模式的四条底层机制行为 对当前 State 的多态调用。Context 对外的方法体只有一行委托真正决定做什么、给什么反馈的是运行时state指针指向的具体状态类。切换状态指针就等于整体更换了行为集合调用方无感知——这便是看起来像修改了类的本质。状态对象持有 Context 引用以驱动转移。四个状态类都在构造时注入gumballMachine并通过setState(...)切换指针。这种状态自己决定后继状态的风格让每个状态类既是行为载体又是转移规则表不需要 Context 维护复杂的转移矩阵。状态对象可共享、可复用。状态对象自身基本不保存可变业务数据唯一的库存count存放在 Context因此机器在构造期一次性创建四个状态对象并反复切换使用即可内存开销恒定。状态机对非法动作是显式建模而非运行时防御。每个非法组合都对应一行拒绝输出任何动作在任何状态下都有确定的行为结果程序不会出现未定义状态。六、状态模式与策略模式的异同仓库姊妹篇状态模式的类图与策略模式高度相似二者都通过组合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),仅供参考
返回列表