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 典型组合使用场景
两者并非互斥,而是可以互补。例如,在订单系统中:
- 用状态机模式管理“待支付→已支付→已发货”的正常流转;
- 用备忘录模式保存每个状态变更前的快照,支持“撤销状态转换”(如用户误操作发货后,可撤销回“已支付”状态)。
四、总结:选对模式,管好状态
- 选备忘录模式:当需要“保存历史状态,支持撤销/恢复”时(如编辑器、游戏存档、事务回滚);
- 选状态机模式:当需要“定义状态转换规则,不同状态有不同行为”时(如订单流转、设备状态、工作流引擎);
- 组合使用:复杂系统中,两者可结合实现“状态流转+历史回溯”的双重能力(如电商订单的“修改地址→撤销修改”)。
状态管理是软件设计的“地基”,备忘录模式和状态机模式分别提供了“保存历史”和“规范流转”的解决方案。理解它们的设计思想和适用边界,能让你的代码在“状态管理”这个关键领域,既灵活又健壮。