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

Java设计模式实战:备忘录模式与状态机模式的“状态管理”双雄

引言

在软件开发中,“状态管理”是永恒的主题:从文本编辑器的“撤销/重做”到电商订单的“待支付→已发货→已完成”流转,从游戏角色的“满血→受伤→濒死”状态切换到数据库事务的“提交/回滚”,如何优雅地处理状态的保存、恢复与转换,直接影响代码的可维护性和扩展性。

本文将深入解析两种与状态管理强相关的设计模式——备忘录模式(Memento Pattern)状态机模式(State Machine Pattern),通过文本编辑器撤销功能、电商订单状态流转两大实战案例,拆解它们的设计逻辑,并对比两者的适用边界,助你在实际开发中“选对模式,管好状态”。


一、备忘录模式:给状态“拍快照”的时光机

1.1 定义与核心角色

备忘录模式的官方定义是:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便后续恢复对象到该状态

它的核心由三个角色组成(图1):

  • Originator(原发器):需要保存状态的对象(如文本编辑器),负责创建/恢复备忘录;
  • Memento(备忘录):存储Originator的状态,通常只允许Originator访问内部细节;
  • Caretaker(管理者):管理备忘录的“保管员”,负责保存、获取备忘录,但不修改其内容。

备忘录模式类图
图1:备忘录模式类图

1.2 实战案例:文本编辑器的撤销功能

假设我们要开发一个支持“撤销”的文本编辑器,用户输入内容后,可通过“Ctrl+Z”回退到上一步状态。用备忘录模式实现的关键是:每次输入后保存当前状态,撤销时恢复最近一次的状态。

1.2.1 定义Memento(备忘录)

备忘录需要保存Originator的关键状态(这里是文本内容),且仅允许Originator访问。通过私有构造+接口限制实现封装:

// 备忘录类(仅允许Originator访问内部状态)
public class TextMemento {private final String content;  // 保存的文本内容// 构造方法私有,仅允许Originator创建TextMemento(String content) {this.content = content;}// 提供给Originator的获取状态方法String getContent() {return content;}
}
1.2.2 定义Originator(文本编辑器)

文本编辑器(Originator)负责创建备忘录(createMemento())和恢复状态(restoreMemento()):

public class TextEditor {private String content = "";  // 当前文本内容// 创建备忘录(保存当前状态)public TextMemento createMemento() {return new TextMemento(content);}// 恢复备忘录(回退到保存状态)public void restoreMemento(TextMemento memento) {this.content = memento.getContent();}// 模拟用户输入(修改状态)public void appendText(String text) {this.content += text;}// 获取当前内容(用于展示)public String getContent() {return content;}
}
1.2.3 定义Caretaker(历史记录管理器)

历史记录管理器(Caretaker)用栈保存备忘录,实现“后进先出”的撤销顺序:

import java.util.Stack;public class HistoryManager {private final Stack<TextMemento> mementoStack = new Stack<>();  // 用栈保存历史状态// 保存新状态(用户输入后调用)public void saveState(TextMemento memento) {mementoStack.push(memento);}// 撤销到上一状态(用户触发Ctrl+Z时调用)public TextMemento undo() {if (mementoStack.isEmpty()) {return null;  // 无历史记录可撤销}return mementoStack.pop();  // 弹出最近一次保存的状态}
}
1.2.4 测试流程
public class MementoDemo {public static void main(String[] args) {TextEditor editor = new TextEditor();HistoryManager history = new HistoryManager();// 用户输入"Hello ",保存状态editor.appendText("Hello ");history.saveState(editor.createMemento());System.out.println("当前内容: " + editor.getContent());  // 输出: Hello // 用户输入"World!",保存状态editor.appendText("World!");history.saveState(editor.createMemento());System.out.println("当前内容: " + editor.getContent());  // 输出: Hello World!// 用户触发撤销(Ctrl+Z)TextMemento undoState = history.undo();if (undoState != null) {editor.restoreMemento(undoState);}System.out.println("撤销后内容: " + editor.getContent());  // 输出: Hello }
}

1.3 备忘录模式的优缺点与适用场景

优点缺点适用场景
状态保存与恢复解耦可能占用较多内存(保存大量状态)需撤销/重做的功能(编辑器、游戏存档)
封装性好(状态由Originator管理)频繁保存可能影响性能事务回滚(数据库、分布式事务)

二、状态机模式:让状态转换“自动机”化

2.1 定义与核心角色

状态机模式(通常指状态模式,State Pattern)的定义是:允许对象在其内部状态改变时改变其行为,对象看起来好像修改了其类。其核心思想是将状态相关的行为封装到独立的状态类中,通过状态切换触发不同行为。

它的核心由三个角色组成(图2):

  • Context(上下文):持有当前状态的引用,将行为委托给当前状态(如订单对象);
  • State(状态接口):定义所有状态的公共行为(如订单的支付、发货操作);
  • ConcreteState(具体状态):实现State接口,处理当前状态下的具体行为,并负责状态转换。

状态模式类图
图2:状态模式类图

2.2 实战案例:电商订单状态流转

电商订单通常有“待支付→已支付→已发货→已完成”的状态流转,不同状态下允许的操作不同(如“待支付”状态可取消,“已发货”状态不可取消)。用状态机模式实现的关键是:将每个状态的行为封装到独立类中,状态切换时自动委托行为。

2.2.1 定义State(订单状态接口)
public interface OrderState {// 支付操作(待支付→已支付)void pay(OrderContext context);// 发货操作(已支付→已发货)void deliver(OrderContext context);// 确认收货(已发货→已完成)void confirm(OrderContext context);// 取消订单(仅部分状态允许)void cancel(OrderContext context);
}
2.2.2 定义ConcreteState(具体状态类)

以“待支付状态(PendingPaymentState)”为例,它允许支付和取消,但不允许发货或确认:

public class PendingPaymentState implements OrderState {@Overridepublic void pay(OrderContext context) {System.out.println("支付成功,订单状态更新为[已支付]");context.setState(new PaidState());  // 支付后切换到已支付状态}@Overridepublic void deliver(OrderContext context) {throw new IllegalStateException("待支付状态不可发货");}@Overridepublic void confirm(OrderContext context) {throw new IllegalStateException("待支付状态不可确认收货");}@Overridepublic void cancel(OrderContext context) {System.out.println("取消成功,订单状态更新为[已取消]");context.setState(new CancelledState());  // 取消后切换到已取消状态}
}

其他状态类(PaidState已支付、DeliveredState已发货、CompletedState已完成、CancelledState已取消)类似,仅实现当前状态允许的操作。

2.2.3 定义Context(订单上下文)

订单上下文(OrderContext)持有当前状态,并提供状态切换的入口:

public class OrderContext {private OrderState currentState;  // 当前状态public OrderContext() {this.currentState = new PendingPaymentState();  // 初始状态为待支付}// 设置新状态(由具体状态类调用)public void setState(OrderState state) {this.currentState = state;}// 暴露给外部的操作入口(委托给当前状态)public void pay() {currentState.pay(this);}public void deliver() {currentState.deliver(this);}public void confirm() {currentState.confirm(this);}public void cancel() {currentState.cancel(this);}
}
2.2.4 测试流程
public class StateMachineDemo {public static void main(String[] args) {OrderContext order = new OrderContext();// 初始状态:待支付order.pay();       // 输出: 支付成功,订单状态更新为[已支付]order.deliver();   // 输出: 发货成功,订单状态更新为[已发货]order.confirm();   // 输出: 确认收货成功,订单状态更新为[已完成]order.cancel();    // 抛出异常: 已完成状态不可取消}
}

2.3 状态机模式的优缺点与适用场景

优点缺点适用场景
状态转换逻辑清晰(每个状态独立)状态类数量可能膨胀(状态多时代码量增加)状态流转复杂的场景(订单、工作流)
符合开闭原则(新增状态只需添加新类)状态切换需谨慎设计(避免循环依赖)设备状态管理(空调、电梯)

三、备忘录模式 vs 状态机模式:状态管理的“左右互搏”

3.1 核心目标不同

  • 备忘录模式:聚焦“状态的保存与恢复”,解决“如何回退到历史状态”的问题(如撤销、回滚);
  • 状态机模式:聚焦“状态的转换与行为”,解决“不同状态下允许哪些操作”的问题(如订单流转、权限控制)。

3.2 状态的生命周期不同

  • 备忘录模式的状态是“静态的”:保存的是某个时间点的快照,恢复时直接覆盖当前状态;
  • 状态机模式的状态是“动态的”:状态之间有严格的转换规则,每个状态定义了允许的操作。

3.3 协作方式不同

  • 备忘录模式依赖Caretaker管理多个历史状态,Originator通过Memento与Caretaker交互;
  • 状态机模式依赖Context委托行为给当前State,State之间通过修改Context的状态引用来切换。

3.4 典型组合使用场景

两者并非互斥,而是可以互补。例如,在订单系统中:

  • 状态机模式管理“待支付→已支付→已发货”的正常流转;
  • 备忘录模式保存每个状态变更前的快照,支持“撤销状态转换”(如用户误操作发货后,可撤销回“已支付”状态)。

四、总结:选对模式,管好状态

  • 选备忘录模式:当需要“保存历史状态,支持撤销/恢复”时(如编辑器、游戏存档、事务回滚);
  • 选状态机模式:当需要“定义状态转换规则,不同状态有不同行为”时(如订单流转、设备状态、工作流引擎);
  • 组合使用:复杂系统中,两者可结合实现“状态流转+历史回溯”的双重能力(如电商订单的“修改地址→撤销修改”)。

状态管理是软件设计的“地基”,备忘录模式和状态机模式分别提供了“保存历史”和“规范流转”的解决方案。理解它们的设计思想和适用边界,能让你的代码在“状态管理”这个关键领域,既灵活又健壮。

http://www.dtcms.com/a/275555.html

相关文章:

  • 基于MCP的CI/CD流水线:自动化部署到云平台的实践
  • 英语单词学习系统
  • 周末总结(2024/07/12)
  • 13. https 是绝对安全的吗
  • 代码审计-Struts2漏洞分析
  • 从LLM到VLM:视觉语言模型的核心技术与Python实现
  • React 组件中怎么做事件代理?它的原理是什么?
  • html-初级标签
  • JAX study notes[17]
  • Java从入门到精通!第四天(面向对象(一))
  • Unity VR手术模拟系统架构分析与数据流设计
  • 【设计模式】装饰(器)模式 透明装饰模式与半透明装饰模式
  • 前端MQTT入门指南:从零到实战的完整流程
  • Google浏览器【无法安装扩展程序,因为它使用了不受支持的清单版本】解决方案
  • 【FreeRTOS】信号量
  • 自助KTV选址指南与优化策略
  • 刘火良 FreeRTOS内核实现与应用之5——补充知识(宏)
  • [Python] -实用技巧篇1-用一行Python代码搞定日常任务
  • Effective Modern C++ 条款9:优先考虑别名声明而非typedef
  • C++法则21:避免将#include放在命名空间内部。
  • Java-71 深入浅出 RPC Dubbo 上手 父工程配置编写 附详细POM与代码
  • Java使用Langchai4j接入AI大模型的简单使用(一)
  • 【跟我学运维】chkconfig jenkins on的含义
  • 使用 Java 开发大数据应用:Hadoop 与 Java API 的结合
  • Gas and Gas Price
  • MCP选型指南:AWS vs Azure vs GCP vs 国内云厂商深度对比
  • 从 Spring 源码到项目实战:设计模式落地经验与最佳实践
  • 批量自动运行多个 Jupyter Notebook 文件的方法!!!
  • 13. G1垃圾回收器
  • Edge浏览器:报告不安全的站点的解决方案