文章目录
-
- [1. 什么是备忘录模式?](#1. 什么是备忘录模式?)
- [2. 为什么需要备忘录模式?](#2. 为什么需要备忘录模式?)
- [3. 备忘录模式的核心概念](#3. 备忘录模式的核心概念)
- [4. 备忘录模式的结构](#4. 备忘录模式的结构)
- [5. 备忘录模式的基本实现](#5. 备忘录模式的基本实现)
-
- [5.1 简单的文本编辑器示例](#5.1 简单的文本编辑器示例)
- [6. 备忘录模式的进阶实现](#6. 备忘录模式的进阶实现)
- [7. 备忘录模式的复杂实现](#7. 备忘录模式的复杂实现)
-
- [7.1 多状态备忘录](#7.1 多状态备忘录)
- [7.2 使用内部类实现](#7.2 使用内部类实现)
- [8. 备忘录模式在Java中的实际应用](#8. 备忘录模式在Java中的实际应用)
-
- [8.1 Java中的序列化与备忘录模式](#8.1 Java中的序列化与备忘录模式)
- [8.2 Java Swing中的撤销/重做功能](#8.2 Java Swing中的撤销/重做功能)
- [9. 备忘录模式与其他设计模式的结合](#9. 备忘录模式与其他设计模式的结合)
-
- [9.1 备忘录模式与命令模式](#9.1 备忘录模式与命令模式)
- [10. 备忘录模式的优缺点](#10. 备忘录模式的优缺点)
-
- [10.1 优点](#10.1 优点)
- [10.2 缺点](#10.2 缺点)
- [11. 备忘录模式的适用场景](#11. 备忘录模式的适用场景)
- [12. 备忘录模式与其他模式的比较](#12. 备忘录模式与其他模式的比较)
-
- [12.1 备忘录模式 vs 原型模式](#12.1 备忘录模式 vs 原型模式)
- [12.2 备忘录模式 vs 命令模式](#12.2 备忘录模式 vs 命令模式)
- [12.3 备忘录模式 vs 状态模式](#12.3 备忘录模式 vs 状态模式)
- [13. 备忘录模式的常见问题与解决方案](#13. 备忘录模式的常见问题与解决方案)
-
- [13.1 如何减少备忘录模式的内存消耗?](#13.1 如何减少备忘录模式的内存消耗?)
- [13.2 如何实现多层撤销/重做?](#13.2 如何实现多层撤销/重做?)
- [13.3 如何处理复杂对象的状态?](#13.3 如何处理复杂对象的状态?)
- [14. 总结](#14. 总结)
-
- [14.1 核心要点](#14.1 核心要点)
- [14.2 设计建议](#14.2 设计建议)
1. 什么是备忘录模式?
备忘录模式是一种行为型设计模式,它允许在不破坏对象封装性的前提下,捕获对象的内部状态,并在需要时将对象恢复到这个状态。简单来说,备忘录模式就是用来实现"撤销"和"恢复"功能的一种设计模式。
备忘录模式使得程序可以回到某个历史状态,就像"时间旅行"一样,让我们能够重返过去的某个时间点。
2. 为什么需要备忘录模式?
在以下情况下,备忘录模式特别有用:
- 需要保存和恢复对象的状态:当我们需要提供撤销操作或回滚机制时
- 直接访问对象的成员变量会破坏封装性:我们需要保存对象状态,但又不希望破坏封装性
- 需要保存对象状态的快照:当我们需要暂时保存某些状态以便后期恢复时
- 事务回滚:当执行一系列操作后需要回滚到原始状态时
- 状态历史记录:当需要维护对象历史状态以便浏览或回溯时
3. 备忘录模式的核心概念
备忘录模式涉及三个核心角色:
-
发起人(Originator):
- 需要保存状态的对象
- 负责创建一个备忘录,存储自身的内部状态
- 可以使用备忘录恢复自身状态
-
备忘录(Memento):
- 存储发起人的内部状态
- 只能被发起人访问,对其他对象不可见
- 保护内部状态不被外部修改
-
管理者(Caretaker):
- 负责保存备忘录
- 不能对备忘录的内容进行操作或检查
- 仅仅是一个存储备忘录的容器
4. 备忘录模式的结构
备忘录模式的UML类图如下:
+---------------+ 创建 +---------------+
| Originator |--------------->| Memento |
+---------------+ +---------------+
| state | | state |
| createMemento()| +---------------+
| restoreMemento()| ^
+---------------+ |
^ |
| |
| 保存/获取 |
| |
+---------------+ |
| Caretaker |----------------------+
+---------------+
| mementoList |
+---------------+
5. 备忘录模式的基本实现
5.1 简单的文本编辑器示例
下面是一个简单的文本编辑器示例,展示了备忘录模式的基本实现。
首先,定义备忘录类:
java
// 备忘录类 - 存储文本编辑器的状态
public class TextEditorMemento {
private final String text;
public TextEditorMemento(String text) {
this.text = text;
}
// 只允许发起人访问状态
protected String getText() {
return text;
}
}
然后,定义发起人类(文本编辑器):
java
// 发起人类 - 文本编辑器
public class TextEditor {
private String text;
public void setText(String text) {
this.text = text;
}
public String getText() {
return text;
}
// 创建备忘录,保存当前状态
public TextEditorMemento save() {
return new TextEditorMemento(text);
}
// 从备忘录恢复状态
public void restore(TextEditorMemento memento) {
this.text = memento.getText();
}
}
最后,定义管理者类:
java
// 管理者类 - 维护备忘录历史
public class TextEditorCaretaker {
private final List<TextEditorMemento> history = new ArrayList<>();
private int currentIndex = -1;
// 保存备忘录
public void save(TextEditorMemento memento) {
// 如果当前不是最后一个状态,则删除当前状态之后的所有状态
if (currentIndex < history.size() - 1) {
history.subList(currentIndex + 1, history.size()).clear();
}
history.add(memento);
currentIndex = history.size() - 1;
}
// 撤销操作,返回上一个状态
public TextEditorMemento undo() {
if (currentIndex <= 0) {
return null; // 无法撤销
}
currentIndex--;
return history.get(currentIndex);
}
// 重做操作,恢复到下一个状态
public TextEditorMemento redo() {
if (currentIndex >= history.size() - 1) {
return null; // 无法重做
}
currentIndex++;
return history.get(currentIndex);
}
}
创建一个测试类,演示备忘录模式:
java
public class TextEditorDemo {
public static void main(String[] args) {
// 创建文本编辑器(发起人)
TextEditor editor = new TextEditor();
// 创建历史记录管理者(管理者)
TextEditorCaretaker caretaker = new TextEditorCaretaker();
// 编辑文本并保存状态
editor.setText("这是第一行文本");
caretaker.save(editor.save());
System.out.println("当前文本: " + editor.getText());
// 继续编辑
editor.setText("这是第一行文本\n这是第二行文本");
caretaker.save(editor.save());
System.out.println("当前文本: " + editor.getText());
// 再次编辑
editor.setText("这是第一行文本\n这是第二行文本\n这是第三行文本");
caretaker.save(editor.save());
System.out.println("当前文本: " + editor.getText());
// 执行撤销操作
TextEditorMemento previousState = caretaker.undo();
if (previousState != null) {
editor.restore(previousState);
System.out.println("\n执行撤销后,文本变为: \n" + editor.getText());
}
// 再次撤销
previousState = caretaker.undo();
if (previousState != null) {
editor.restore(previousState);
System.out.println("\n再次撤销后,文本变为: \n" + editor.getText());
}
// 执行重做
TextEditorMemento nextState = caretaker.redo();
if (nextState != null) {
editor.restore(nextState);
System.out.println("\n执行重做后,文本变为: \n" + editor.getText());
}
// 再次重做
nextState = caretaker.redo();
if (nextState != null) {
editor.restore(nextState);
System.out.println("\n再次重做后,文本变为: \n" + editor.getText());
}
}
}
输出结果:
当前文本: 这是第一行文本
当前文本: 这是第一行文本
这是第二行文本
当前文本: 这是第一行文本
这是第二行文本
这是第三行文本
执行撤销后,文本变为:
这是第一行文本
这是第二行文本
再次撤销后,文本变为:
这是第一行文本
执行重做后,文本变为:
这是第一行文本
这是第二行文本
再次重做后,文本变为:
这是第一行文本
这是第二行文本
这是第三行文本
6. 备忘录模式的进阶实现
6.1 游戏角色状态保存示例
下面我们实现一个更复杂的例子:游戏角色状态的保存和恢复。
首先,定义备忘录类:
java
// 游戏角色状态备忘录
public class RoleStateMemento {
private final int vitality; // 生命力
private final int attack; // 攻击力
private final int defense; // 防御力
public RoleStateMemento(int vitality, int attack, int defense) {
this.vitality = vitality;
this.attack = attack;
this.defense = defense;
}
// 提供保护性的访问方法
protected int getVitality() {
return vitality;
}
protected int getAttack() {
return attack;
}
protected int getDefense() {
return defense;
}
}
然后,定义发起人类(游戏角色):
java
// 游戏角色类(发起人)
public class GameRole {
private String name; // 角色名称
private int vitality; // 生命力
private int attack; // 攻击力
private int defense; // 防御力
public GameRole(String name) {
this.name = name;
this.vitality = 100; // 初始生命值
this.attack = 50; // 初始攻击力
this.defense = 30; // 初始防御力
}
// 创建备忘录,保存当前状态
public RoleStateMemento saveState() {
return new RoleStateMemento(vitality, attack, defense);
}
// 从备忘录恢复状态
public void restoreState(RoleStateMemento memento) {
this.vitality = memento.getVitality();
this.attack = memento.getAttack();
this.defense = memento.getDefense();
}
// 战斗,消耗生命值和防御力,提高攻击力
public void fight() {
this.vitality -= 30;
this.attack += 20;
this.defense -= 10;
}
// 使用药水,恢复生命值
public void usePotion() {
this.vitality += 40;
}
// 显示角色状态
public void displayState() {
System.out.println("角色: " + name);
System.out.println("生命力: " + vitality);
System.out.println("攻击力: " + attack);
System.out.println("防御力: " + defense);
System.out.println("--------------------");
}
}
最后,定义管理者类:
java
// 游戏存档管理器(管理者)
public class GameStateCaretaker {
private Map<String, RoleStateMemento> saveSlots = new HashMap<>();
// 保存游戏状态到指定存档
public void saveState(String slotName, RoleStateMemento memento) {
saveSlots.put(slotName, memento);
}
// 从指定存档加载游戏状态
public RoleStateMemento loadState(String slotName) {
return saveSlots.get(slotName);
}
// 显示所有存档
public void displaySaveSlots() {
System.out.println("可用存档: ");
for (String slotName : saveSlots.keySet()) {
System.out.println("- " + slotName);
}
System.out.println("--------------------");
}
}
创建测试类,演示游戏存档功能:
java
public class GameDemo {
public static void main(String[] args) {
// 创建游戏角色
GameRole knight = new GameRole("骑士");
// 创建游戏存档管理器
GameStateCaretaker caretaker = new GameStateCaretaker();
// 显示初始状态
System.out.println("游戏开始,初始状态:");
knight.displayState();
// 保存初始状态
caretaker.saveState("初始存档", knight.saveState());
// 进行一场战斗
System.out.println("进行一场战斗后:");
knight.fight();
knight.displayState();
// 保存战斗后状态
caretaker.saveState("战斗后存档", knight.saveState());
// 使用药水恢复
System.out.println("使用药水恢复后:");
knight.usePotion();
knight.displayState();
// 保存使用药水后状态
caretaker.saveState("恢复后存档", knight.saveState());
// 显示所有存档
caretaker.displaySaveSlots();
// 从初始存档加载
System.out.println("加载初始存档:");
knight.restoreState(caretaker.loadState("初始存档"));
knight.displayState();
// 从战斗后存档加载
System.out.println("加载战斗后存档:");
knight.restoreState(caretaker.loadState("战斗后存档"));
knight.displayState();
// 从恢复后存档加载
System.out.println("加载恢复后存档:");
knight.restoreState(caretaker.loadState("恢复后存档"));
knight.displayState();
}
}
输出结果:
游戏开始,初始状态:
角色: 骑士
生命力: 100
攻击力: 50
防御力: 30
--------------------
进行一场战斗后:
角色: 骑士
生命力: 70
攻击力: 70
防御力: 20
--------------------
使用药水恢复后:
角色: 骑士
生命力: 110
攻击力: 70
防御力: 20
--------------------
可用存档:
- 战斗后存档
- 初始存档
- 恢复后存档
--------------------
加载初始存档:
角色: 骑士
生命力: 100
攻击力: 50
防御力: 30
--------------------
加载战斗后存档:
角色: 骑士
生命力: 70
攻击力: 70
防御力: 20
--------------------
加载恢复后存档:
角色: 骑士
生命力: 110
攻击力: 70
防御力: 20
--------------------
6.2 备忘录模式的不同实现方式
备忘录模式有两种主要实现方式:黑箱模式和白箱模式。
黑箱模式
在黑箱实现中,备忘录类对外隐藏所有状态信息,只有发起人可以访问这些信息。
java
// 黑箱模式的备忘录类
public class BlackBoxMemento {
// 私有的状态字段
private final String state;
public BlackBoxMemento(String state) {
this.state = state;
}
// 外部类只能看到这是一个备忘录对象,无法访问其内容
// 只有发起人可以通过强制类型转换恢复状态
}
// 发起人类
public class Originator {
private String state;
public void setState(String state) {
this.state = state;
}
// 创建备忘录
public BlackBoxMemento saveToMemento() {
return new BlackBoxMemento(state);
}
// 恢复状态 - 发起人知道备忘录的内部结构
public void restoreFromMemento(BlackBoxMemento memento) {
// 此处发起人可以访问备忘录内部状态
this.state = ((BlackBoxMemento)memento).state;
}
}
白箱模式
在白箱实现中,备忘录类提供公共的接口来访问其状态,但这样做会破坏封装性。
java
// 白箱模式的备忘录类
public class WhiteBoxMemento {
private String state;
public WhiteBoxMemento(String state) {
this.state = state;
}
// 提供公共访问方法
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
}
// 发起人类
public class Originator {
private String state;
public void setState(String state) {
this.state = state;
}
// 创建备忘录
public WhiteBoxMemento saveToMemento() {
return new WhiteBoxMemento(state);
}
// 恢复状态
public void restoreFromMemento(WhiteBoxMemento memento) {
this.state = memento.getState();
}
}
一般来说,黑箱模式更符合封装原则,但在Java中实现起来可能会更复杂。而白箱模式虽然违反了封装原则,但实现更简单直接。
7. 备忘录模式的复杂实现
7.1 多状态备忘录
有时我们需要保存对象的多个属性,并且希望能够选择性地恢复这些属性。下面是一个多状态备忘录的实现示例:
java
// 多状态备忘录类 - 可以选择性保存和恢复状态
public class DocumentMemento {
private final String content; // 文档内容
private final String title; // 文档标题
private final String formatting; // 文档格式
private final Date lastModified; // 最后修改时间
// 构造函数
public DocumentMemento(String content, String title, String formatting, Date lastModified) {
this.content = content;
this.title = title;
this.formatting = formatting;
this.lastModified = lastModified != null ? new Date(lastModified.getTime()) : null;
}
// 获取状态的保护性方法
protected String getContent() {
return content;
}
protected String getTitle() {
return title;
}
protected String getFormatting() {
return formatting;
}
protected Date getLastModified() {
return lastModified != null ? new Date(lastModified.getTime()) : null;
}
}
发起人类(文档):
java
// 文档类 - 发起人
public class Document {
private String content; // 文档内容
private String title; // 文档标题
private String formatting; // 文档格式
private Date lastModified; // 最后修改时间
public Document(String title) {
this.title = title;
this.content = "";
this.formatting = "normal";
this.lastModified = new Date();
}
// 添加内容
public void addContent(String newContent) {
this.content += newContent;
this.lastModified = new Date();
}
// 改变标题
public void changeTitle(String newTitle) {
this.title = newTitle;
this.lastModified = new Date();
}
// 改变格式
public void changeFormatting(String newFormatting) {
this.formatting = newFormatting;
this.lastModified = new Date();
}
// 显示文档信息
public void display() {
System.out.println("文档: " + title);
System.out.println("最后修改时间: " + lastModified);
System.out.println("格式: " + formatting);
System.out.println("内容: \n" + content);
System.out.println("--------------------");
}
// 创建完整备忘录
public DocumentMemento createFullMemento() {
return new DocumentMemento(content, title, formatting, lastModified);
}
// 创建只包含内容的备忘录
public DocumentMemento createContentMemento() {
return new DocumentMemento(content, title, null, null);
}
// 创建只包含格式的备忘录
public DocumentMemento createFormattingMemento() {
return new DocumentMemento(null, null, formatting, null);
}
// 从备忘录恢复状态 - 选择性恢复
public void restoreFromMemento(DocumentMemento memento) {
if (memento.getContent() != null) {
this.content = memento.getContent();
}
if (memento.getTitle() != null) {
this.title = memento.getTitle();
}
if (memento.getFormatting() != null) {
this.formatting = memento.getFormatting();
}
if (memento.getLastModified() != null) {
this.lastModified = memento.getLastModified();
} else {
// 如果没有恢复时间,则更新为当前时间
this.lastModified = new Date();
}
}
}
管理者类:
java
// 文档版本管理器 - 管理者
public class DocumentCaretaker {
// 使用Map存储不同类型的备忘录
private Map<String, List<DocumentMemento>> versionHistory = new HashMap<>();
// 初始化版本历史记录
public DocumentCaretaker() {
versionHistory.put("full", new ArrayList<>());
versionHistory.put("content", new ArrayList<>());
versionHistory.put("formatting", new ArrayList<>());
}
// 保存完整版本
public void saveFullVersion(DocumentMemento memento) {
versionHistory.get("full").add(memento);
}
// 保存内容版本
public void saveContentVersion(DocumentMemento memento) {
versionHistory.get("content").add(memento);
}
// 保存格式版本
public void saveFormattingVersion(DocumentMemento memento) {
versionHistory.get("formatting").add(memento);
}
// 获取最新的完整版本
public DocumentMemento getLatestFullVersion() {
List<DocumentMemento> fullVersions = versionHistory.get("full");
if (fullVersions.isEmpty()) {
return null;
}
return fullVersions.get(fullVersions.size() - 1);
}
// 获取最新的内容版本
public DocumentMemento getLatestContentVersion() {
List<DocumentMemento> contentVersions = versionHistory.get("content");
if (contentVersions.isEmpty()) {
return null;
}
return contentVersions.get(contentVersions.size() - 1);
}
// 获取最新的格式版本
public DocumentMemento getLatestFormattingVersion() {
List<DocumentMemento> formattingVersions = versionHistory.get("formatting");
if (formattingVersions.isEmpty()) {
return null;
}
return formattingVersions.get(formattingVersions.size() - 1);
}
// 显示所有版本历史数量
public void displayVersionInfo() {
System.out.println("版本历史信息:");
System.out.println("完整版本数: " + versionHistory.get("full").size());
System.out.println("内容版本数: " + versionHistory.get("content").size());
System.out.println("格式版本数: " + versionHistory.get("formatting").size());
System.out.println("--------------------");
}
}
测试类:
java
public class MultiStateDocumentDemo {
public static void main(String[] args) {
// 创建文档
Document document = new Document("未命名文档");
// 创建文档版本管理器
DocumentCaretaker caretaker = new DocumentCaretaker();
// 显示初始状态
System.out.println("文档初始状态:");
document.display();
// 保存初始完整版本
caretaker.saveFullVersion(document.createFullMemento());
// 添加内容
document.addContent("这是第一段内容。");
document.display();
// 保存内容版本
caretaker.saveContentVersion(document.createContentMemento());
// 改变标题和内容
document.changeTitle("我的文档");
document.addContent("\n这是第二段内容。");
document.display();
// 保存完整版本
caretaker.saveFullVersion(document.createFullMemento());
// 改变格式
document.changeFormatting("bold");
document.display();
// 保存格式版本
caretaker.saveFormattingVersion(document.createFormattingMemento());
// 显示版本信息
caretaker.displayVersionInfo();
// 恢复到最新的内容版本(只恢复内容,不恢复标题和格式)
System.out.println("恢复到最新的内容版本(只恢复内容):");
document.restoreFromMemento(caretaker.getLatestContentVersion());
document.display();
// 恢复到最新的格式版本(只恢复格式)
System.out.println("恢复到最新的格式版本(只恢复格式):");
document.restoreFromMemento(caretaker.getLatestFormattingVersion());
document.display();
// 恢复到最新的完整版本(恢复所有属性)
System.out.println("恢复到最新的完整版本(恢复所有属性):");
document.restoreFromMemento(caretaker.getLatestFullVersion());
document.display();
}
}
7.2 使用内部类实现
在Java中,我们可以使用内部类来实现备忘录模式,这样可以更好地保护备忘录的状态不被外部访问。
java
// 发起人类 - 使用内部类作为备忘录
public class EditorWithInnerMemento {
private String content;
public void setContent(String content) {
this.content = content;
}
public String getContent() {
return content;
}
// 创建备忘录
public Memento save() {
return new Memento(content);
}
// 从备忘录恢复
public void restore(Memento memento) {
content = memento.getSavedContent();
}
// 内部类实现备忘录
public class Memento {
private final String savedContent;
private Memento(String contentToSave) {
savedContent = contentToSave;
}
private String getSavedContent() {
return savedContent;
}
}
}
使用内部类的管理者类:
java
// 管理者类
public class InnerMementoCaretaker {
private List<EditorWithInnerMemento.Memento> history = new ArrayList<>();
public void push(EditorWithInnerMemento.Memento memento) {
history.add(memento);
}
public EditorWithInnerMemento.Memento pop() {
if (history.isEmpty()) {
return null;
}
EditorWithInnerMemento.Memento lastMemento = history.get(history.size() - 1);
history.remove(history.size() - 1);
return lastMemento;
}
}
测试内部类实现:
java
public class InnerMementoDemo {
public static void main(String[] args) {
EditorWithInnerMemento editor = new EditorWithInnerMemento();
InnerMementoCaretaker caretaker = new InnerMementoCaretaker();
// 设置初始内容
editor.setContent("第一版内容");
System.out.println("初始内容: " + editor.getContent());
// 保存状态
caretaker.push(editor.save());
// 修改内容
editor.setContent("第二版内容");
System.out.println("修改后内容: " + editor.getContent());
// 保存状态
caretaker.push(editor.save());
// 再次修改内容
editor.setContent("第三版内容");
System.out.println("再次修改后内容: " + editor.getContent());
// 恢复到上一个状态
editor.restore(caretaker.pop());
System.out.println("恢复到上一个状态后内容: " + editor.getContent());
// 再次恢复到上一个状态
editor.restore(caretaker.pop());
System.out.println("再次恢复后内容: " + editor.getContent());
}
}
8. 备忘录模式在Java中的实际应用
8.1 Java中的序列化与备忘录模式
Java提供的序列化机制可以看作是备忘录模式的一种实现形式,它允许我们将对象的状态保存到文件中,并在需要时恢复。
java
import java.io.*;
// 可序列化对象 - 相当于发起人
public class SerializableGameState implements Serializable {
private static final long serialVersionUID = 1L;
private String playerName;
private int level;
private int score;
private List<String> inventory;
public SerializableGameState(String playerName) {
this.playerName = playerName;
this.level = 1;
this.score = 0;
this.inventory = new ArrayList<>();
}
// 游戏进展
public void advanceLevel() {
level++;
score += 1000;
}
// 增加分数
public void addScore(int points) {
score += points;
}
// 添加物品到库存
public void addItemToInventory(String item) {
inventory.add(item);
}
// 显示游戏状态
public void displayState() {
System.out.println("玩家: " + playerName);
System.out.println("等级: " + level);
System.out.println("分数: " + score);
System.out.println("物品库存: " + inventory);
System.out.println("--------------------");
}
// 保存状态到文件 - 使用序列化
public void saveToFile(String fileName) {
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName))) {
oos.writeObject(this);
System.out.println("游戏状态已保存到: " + fileName);
} catch (IOException e) {
System.out.println("保存游戏状态失败: " + e.getMessage());
}
}
// 从文件加载状态 - 使用反序列化
public static SerializableGameState loadFromFile(String fileName) {
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName))) {
SerializableGameState gameState = (SerializableGameState) ois.readObject();
System.out.println("游戏状态已从: " + fileName + " 加载");
return gameState;
} catch (IOException | ClassNotFoundException e) {
System.out.println("加载游戏状态失败: " + e.getMessage());
return null;
}
}
}
测试序列化实现:
java
public class SerializationMementoDemo {
public static void main(String[] args) {
String saveFile = "game_save.dat";
// 创建新游戏
SerializableGameState game = new SerializableGameState("玩家1");
System.out.println("新游戏创建:");
game.displayState();
// 游戏进展
game.advanceLevel();
game.addScore(500);
game.addItemToInventory("剑");
game.addItemToInventory("盾");
System.out.println("\n游戏进展后:");
game.displayState();
// 保存游戏状态
game.saveToFile(saveFile);
// 继续游戏
game.advanceLevel();
game.addScore(800);
game.addItemToInventory("药水");
System.out.println("\n继续游戏后:");
game.displayState();
// 加载之前保存的游戏状态
SerializableGameState loadedGame = SerializableGameState.loadFromFile(saveFile);
if (loadedGame != null) {
System.out.println("\n加载存档后:");
loadedGame.displayState();
}
}
}
8.2 Java Swing中的撤销/重做功能
Java Swing的UndoManager
类提供了撤销/重做功能,这是备忘录模式的另一种实现。
java
import javax.swing.*;
import javax.swing.text.*;
import javax.swing.undo.*;
import java.awt.*;
import java.awt.event.*;
public class UndoRedoTextEditor extends JFrame {
private JTextArea textArea;
private UndoManager undoManager;
public UndoRedoTextEditor() {
// 设置窗口
setTitle("带撤销/重做功能的文本编辑器");
setSize(500, 400);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 创建文本区域
textArea = new JTextArea();
JScrollPane scrollPane = new JScrollPane(textArea);
add(scrollPane, BorderLayout.CENTER);
// 创建撤销管理器
undoManager = new UndoManager();
// 监听文本变化
Document doc = textArea.getDocument();
doc.addUndoableEditListener(e -> undoManager.addEdit(e.getEdit()));
// 创建工具栏
JToolBar toolBar = new JToolBar();
// 创建撤销按钮
JButton undoButton = new JButton("撤销");
undoButton.addActionListener(e -> {
try {
if (undoManager.canUndo()) {
undoManager.undo();
}
} catch (CannotUndoException ex) {
System.out.println("无法撤销: " + ex);
}
});
// 创建重做按钮
JButton redoButton = new JButton("重做");
redoButton.addActionListener(e -> {
try {
if (undoManager.canRedo()) {
undoManager.redo();
}
} catch (CannotRedoException ex) {
System.out.println("无法重做: " + ex);
}
});
// 添加按钮到工具栏
toolBar.add(undoButton);
toolBar.add(redoButton);
// 添加工具栏到窗口
add(toolBar, BorderLayout.NORTH);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
UndoRedoTextEditor editor = new UndoRedoTextEditor();
editor.setVisible(true);
});
}
}
9. 备忘录模式与其他设计模式的结合
9.1 备忘录模式与命令模式
备忘录模式经常与命令模式结合使用,特别是在需要撤销操作的场景中。命令模式封装操作,而备忘录模式保存状态。
java
// 命令接口
interface Command {
void execute();
void undo();
}
// 接收者类
class Calculator {
private int currentValue;
public Calculator() {
this.currentValue = 0;
}
public void add(int value) {
currentValue += value;
}
public void subtract(int value) {
currentValue -= value;
}
public int getCurrentValue() {
return currentValue;
}
public void setCurrentValue(int value) {
this.currentValue = value;
}
// 备忘录内部类
public class CalculatorMemento {
private final int savedValue;
private CalculatorMemento(int value) {
this.savedValue = value;
}
private int getSavedValue() {
return savedValue;
}
}
// 创建备忘录
public CalculatorMemento createMemento() {
return new CalculatorMemento(currentValue);
}
// 从备忘录恢复状态
public void restoreMemento(CalculatorMemento memento) {
this.currentValue = memento.getSavedValue();
}
}
// 具体命令 - 加法
class AddCommand implements Command {
private Calculator calculator;
private int valueToAdd;
private Calculator.CalculatorMemento memento;
public AddCommand(Calculator calculator, int valueToAdd) {
this.calculator = calculator;
this.valueToAdd = valueToAdd;
}
@Override
public void execute() {
memento = calculator.createMemento(); // 保存当前状态
calculator.add(valueToAdd);
}
@Override
public void undo() {
calculator.restoreMemento(memento); // 恢复到之前的状态
}
}
// 具体命令 - 减法
class SubtractCommand implements Command {
private Calculator calculator;
private int valueToSubtract;
private Calculator.CalculatorMemento memento;
public SubtractCommand(Calculator calculator, int valueToSubtract) {
this.calculator = calculator;
this.valueToSubtract = valueToSubtract;
}
@Override
public void execute() {
memento = calculator.createMemento(); // 保存当前状态
calculator.subtract(valueToSubtract);
}
@Override
public void undo() {
calculator.restoreMemento(memento); // 恢复到之前的状态
}
}
// 调用者 - 计算器操作管理器
class CalculatorInvoker {
private Stack<Command> commandHistory = new Stack<>();
public void executeCommand(Command command) {
command.execute();
commandHistory.push(command);
}
public void undoLastCommand() {
if (!commandHistory.isEmpty()) {
Command lastCommand = commandHistory.pop();
lastCommand.undo();
} else {
System.out.println("没有可撤销的命令");
}
}
}
// 测试类
public class CommandMementoDemo {
public static void main(String[] args) {
Calculator calculator = new Calculator();
CalculatorInvoker invoker = new CalculatorInvoker();
System.out.println("初始值: " + calculator.getCurrentValue());
// 执行加法命令
Command addCommand = new AddCommand(calculator, 10);
invoker.executeCommand(addCommand);
System.out.println("执行加法后: " + calculator.getCurrentValue());
// 执行减法命令
Command subtractCommand = new SubtractCommand(calculator, 5);
invoker.executeCommand(subtractCommand);
System.out.println("执行减法后: " + calculator.getCurrentValue());
// 撤销最后一个命令
invoker.undoLastCommand();
System.out.println("撤销后: " + calculator.getCurrentValue());
// 再次撤销
invoker.undoLastCommand();
System.out.println("再次撤销后: " + calculator.getCurrentValue());
}
}
10. 备忘录模式的优缺点
10.1 优点
-
保护封装性:备忘录模式可以保存对象的状态,同时不破坏对象的封装性。发起人对象可以决定哪些状态需要保存。
-
提供了可靠的恢复机制:通过备忘录模式,系统可以回到某个历史状态,提供了可靠的撤销/恢复功能。
-
降低发起人的复杂度:将状态存储和恢复的逻辑分离出来,发起人类不需要关心怎样保存历史状态。
-
支持事务操作:备忘录模式可以实现简单的事务操作,使得在操作失败时能够回滚到安全状态。
-
简化了发起人:发起人不需要保存自身的历史状态,也不需要管理这些历史版本。
10.2 缺点
-
资源消耗:如果需要保存的状态太大或者保存频率过高,可能会导致内存资源的大量占用。
-
管理者的膨胀:如果备忘录对象太多,管理者可能会变得非常复杂,占用大量内存。
-
额外的开发成本:需要为每个需要保存的对象创建相应的备忘录类。
-
性能问题:频繁地创建和恢复备忘录可能会影响系统性能。
-
可能增加复杂性:在某些情况下,增加备忘录模式可能会增加系统的复杂性,尤其是当发起人的状态非常复杂时。
11. 备忘录模式的适用场景
备忘录模式适用于以下场景:
-
需要提供撤销功能的应用程序:文本编辑器、图形编辑器等需要支持撤销/重做操作的应用。
-
需要保存历史快照的场景:如游戏存档、版本控制工具等需要保存特定时间点状态的场景。
-
需要事务回滚的系统:某些操作需要保证原子性,当操作失败时需要回滚到操作前的状态。
-
需要在不破坏封装的前提下保存和恢复对象状态:备忘录模式允许在不暴露对象内部结构的情况下操作其状态。
-
希望预防用户数据丢失:应用程序可以定期自动保存用户工作状态,以防意外关闭导致数据丢失。
-
模拟可能需要重复执行的场景:模拟实验或游戏场景,可能需要从特定节点重新开始。
12. 备忘录模式与其他模式的比较
12.1 备忘录模式 vs 原型模式
- 备忘录模式:关注在不破坏对象封装的前提下,保存和恢复对象的状态。备忘录对象通常只存储必要的状态信息。
- 原型模式:关注创建对象的完整副本。原型通常会复制对象的所有属性。
主要区别:
- 备忘录模式用于保存和恢复状态,原型模式用于创建对象的副本。
- 备忘录通常只存储必要的状态,而原型复制整个对象。
12.2 备忘录模式 vs 命令模式
- 备忘录模式:关注对象状态的存储和恢复。
- 命令模式:关注操作的封装,可以支持撤销/重做操作。
主要区别:
- 备忘录保存状态,命令保存动作。
- 命令模式通常结合备忘录模式使用,以提供基于状态的撤销功能。
12.3 备忘录模式 vs 状态模式
- 备忘录模式:允许在不破坏封装的前提下捕获对象的内部状态,并在稍后恢复它。
- 状态模式:允许对象在其内部状态改变时改变其行为。
主要区别:
- 备忘录模式关注状态的存储和恢复,状态模式关注基于状态的行为变化。
- 状态模式通常不包含恢复到之前状态的机制。
13. 备忘录模式的常见问题与解决方案
13.1 如何减少备忘录模式的内存消耗?
问题:在一些应用场景中,备忘录可能会占用大量内存资源,尤其是当需要保存大量状态或对象状态非常大时。
解决方案:
- 增量备忘录:只保存状态的变化部分,而不是完整状态。
- 压缩备忘录:对备忘录中的数据进行压缩存储。
- 惰性保存:根据实际需要保存状态,而不是定期保存。
- 限制历史记录数量:设置一个最大历史记录数,超出时删除最旧的记录。
- 使用外部存储:将备忘录保存到文件或数据库,而不是内存中。
java
// 增量备忘录示例
public class IncrementalMemento {
private Map<String, Object> changedProperties = new HashMap<>();
private IncrementalMemento previousMemento;
public void setProperty(String propertyName, Object propertyValue) {
changedProperties.put(propertyName, propertyValue);
}
public Object getProperty(String propertyName) {
if (changedProperties.containsKey(propertyName)) {
return changedProperties.get(propertyName);
} else if (previousMemento != null) {
return previousMemento.getProperty(propertyName);
}
return null;
}
public void setPreviousMemento(IncrementalMemento previousMemento) {
this.previousMemento = previousMemento;
}
}
13.2 如何实现多层撤销/重做?
问题:简单的备忘录模式通常只提供单一的撤销/恢复功能,但实际应用可能需要多层次的撤销/重做支持。
解决方案:
- 使用两个栈:分别用于撤销和重做操作。
- 记录操作序列:保存完整的操作序列,支持在任意点恢复。
- 命令模式结合:使用命令模式记录操作,备忘录模式记录状态。
java
// 多层撤销/重做管理器
public class MultiLevelUndoRedoManager<T> {
private Stack<T> undoStack = new Stack<>();
private Stack<T> redoStack = new Stack<>();
public void saveState(T memento) {
undoStack.push(memento);
// 清空重做栈,因为有新的状态
redoStack.clear();
}
public T undo() {
if (undoStack.isEmpty()) {
return null;
}
T memento = undoStack.pop();
redoStack.push(memento);
return undoStack.isEmpty() ? null : undoStack.peek();
}
public T redo() {
if (redoStack.isEmpty()) {
return null;
}
T memento = redoStack.pop();
undoStack.push(memento);
return memento;
}
public boolean canUndo() {
return undoStack.size() > 1; // 至少需要两个状态才能撤销
}
public boolean canRedo() {
return !redoStack.isEmpty();
}
}
13.3 如何处理复杂对象的状态?
问题:当对象的状态非常复杂,包含多个相互关联的对象时,备忘录模式的实现会变得复杂。
解决方案:
- 组合备忘录:为每个子对象创建单独的备忘录,然后组合起来。
- 序列化:使用Java的序列化机制来保存和恢复复杂对象状态。
- 选择性备忘录:只保存关键的状态信息,而不是所有状态。
- 分层备忘录:为对象的不同层次创建不同的备忘录。
java
// 组合备忘录示例
public class CompositeMemento {
private Map<String, Memento> componentMementos = new HashMap<>();
public void addComponentMemento(String componentId, Memento memento) {
componentMementos.put(componentId, memento);
}
public Memento getComponentMemento(String componentId) {
return componentMementos.get(componentId);
}
// 通用接口
public interface Memento {
void restore(Object originator);
}
}
14. 总结
备忘录模式是一种行为型设计模式,它允许在不破坏对象封装性的前提下,捕获对象的内部状态,并在需要时将对象恢复到这个状态。
14.1 核心要点
-
角色划分:备忘录模式包含发起人、备忘录和管理者三个核心角色,各司其职。
-
封装保护:备忘录模式通过限制对备忘录内部状态的访问,保证了对象的封装性。
-
状态恢复:备忘录模式提供了可靠的状态存储和恢复机制,支持撤销/重做等功能。
-
应用广泛:备忘录模式在文本编辑器、游戏存档、事务系统等多种场景中有广泛应用。
-
实现灵活:备忘录模式有多种实现方式,如黑箱模式、白箱模式、内部类实现等。
14.2 设计建议
-
轻量化备忘录:尽量只保存必要的状态信息,减小备忘录的大小。
-
控制备忘录数量:设置合理的备忘录保存策略,避免过多备忘录占用资源。
-
结合其他模式:考虑与命令模式、原型模式等结合使用,解决更复杂的问题。
-
注意性能问题:在频繁创建或恢复备忘录的场景中,注意性能优化。
-
保护隐私数据:确保备忘录中的敏感数据不会被外部访问。
备忘录模式虽然概念简单,但应用灵活,是实现状态管理、操作撤销等功能的有力工具。通过合理使用备忘录模式,我们可以使应用程序更加健壮,提供更好的用户体验。