文章目录
- 1. 什么是备忘录模式?
- 2. 为什么需要备忘录模式?
- 3. 备忘录模式的核心概念
- 4. 备忘录模式的结构
- 5. 备忘录模式的基本实现
- 5.1 简单的文本编辑器示例
- 6. 备忘录模式的进阶实现
- 6.1 游戏角色状态保存示例
- 6.2 备忘录模式的不同实现方式
- 黑箱模式
- 白箱模式
- 7. 备忘录模式的复杂实现
- 7.1 多状态备忘录
- 7.2 使用内部类实现
- 8. 备忘录模式在Java中的实际应用
- 8.1 Java中的序列化与备忘录模式
- 8.2 Java Swing中的撤销/重做功能
- 9. 备忘录模式与其他设计模式的结合
- 9.1 备忘录模式与命令模式
- 10. 备忘录模式的优缺点
- 10.1 优点
- 10.2 缺点
- 11. 备忘录模式的适用场景
- 12. 备忘录模式与其他模式的比较
- 12.1 备忘录模式 vs 原型模式
- 12.2 备忘录模式 vs 命令模式
- 12.3 备忘录模式 vs 状态模式
- 13. 备忘录模式的常见问题与解决方案
- 13.1 如何减少备忘录模式的内存消耗?
- 13.2 如何实现多层撤销/重做?
- 13.3 如何处理复杂对象的状态?
- 14. 总结
- 14.1 核心要点
- 14.2 设计建议
1. 什么是备忘录模式?
备忘录模式是一种行为型设计模式,它允许在不破坏对象封装性的前提下,捕获对象的内部状态,并在需要时将对象恢复到这个状态。简单来说,备忘录模式就是用来实现"撤销"和"恢复"功能的一种设计模式。
备忘录模式使得程序可以回到某个历史状态,就像"时间旅行"一样,让我们能够重返过去的某个时间点。
2. 为什么需要备忘录模式?
在以下情况下,备忘录模式特别有用:
- 需要保存和恢复对象的状态:当我们需要提供撤销操作或回滚机制时
- 直接访问对象的成员变量会破坏封装性:我们需要保存对象状态,但又不希望破坏封装性
- 需要保存对象状态的快照:当我们需要暂时保存某些状态以便后期恢复时
- 事务回滚:当执行一系列操作后需要回滚到原始状态时
- 状态历史记录:当需要维护对象历史状态以便浏览或回溯时
3. 备忘录模式的核心概念
备忘录模式涉及三个核心角色:
-
发起人(Originator):
- 需要保存状态的对象
- 负责创建一个备忘录,存储自身的内部状态
- 可以使用备忘录恢复自身状态
-
备忘录(Memento):
- 存储发起人的内部状态
- 只能被发起人访问,对其他对象不可见
- 保护内部状态不被外部修改
-
管理者(Caretaker):
- 负责保存备忘录
- 不能对备忘录的内容进行操作或检查
- 仅仅是一个存储备忘录的容器
4. 备忘录模式的结构
备忘录模式的UML类图如下:
+---------------+ 创建 +---------------+
| Originator |--------------->| Memento |
+---------------+ +---------------+
| state | | state |
| createMemento()| +---------------+
| restoreMemento()| ^
+---------------+ |^ || || 保存/获取 || |
+---------------+ |
| Caretaker |----------------------+
+---------------+
| mementoList |
+---------------+
5. 备忘录模式的基本实现
5.1 简单的文本编辑器示例
下面是一个简单的文本编辑器示例,展示了备忘录模式的基本实现。
首先,定义备忘录类:
// 备忘录类 - 存储文本编辑器的状态
public class TextEditorMemento {private final String text;public TextEditorMemento(String text) {this.text = text;}// 只允许发起人访问状态protected String getText() {return text;}
}
然后,定义发起人类(文本编辑器):
// 发起人类 - 文本编辑器
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();}
}
最后,定义管理者类:
// 管理者类 - 维护备忘录历史
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);}
}
创建一个测试类,演示备忘录模式:
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 游戏角色状态保存示例
下面我们实现一个更复杂的例子:游戏角色状态的保存和恢复。
首先,定义备忘录类:
// 游戏角色状态备忘录
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;}
}
然后,定义发起人类(游戏角色):
// 游戏角色类(发起人)
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("--------------------");}
}
最后,定义管理者类:
// 游戏存档管理器(管理者)
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("--------------------");}
}
创建测试类,演示游戏存档功能:
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 备忘录模式的不同实现方式
备忘录模式有两种主要实现方式:黑箱模式和白箱模式。
黑箱模式
在黑箱实现中,备忘录类对外隐藏所有状态信息,只有发起人可以访问这些信息。
// 黑箱模式的备忘录类
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;}
}
白箱模式
在白箱实现中,备忘录类提供公共的接口来访问其状态,但这样做会破坏封装性。
// 白箱模式的备忘录类
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 多状态备忘录
有时我们需要保存对象的多个属性,并且希望能够选择性地恢复这些属性。下面是一个多状态备忘录的实现示例:
// 多状态备忘录类 - 可以选择性保存和恢复状态
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;}
}
发起人类(文档):
// 文档类 - 发起人
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();}}
}
管理者类:
// 文档版本管理器 - 管理者
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("--------------------");}
}
测试类:
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中,我们可以使用内部类来实现备忘录模式,这样可以更好地保护备忘录的状态不被外部访问。
// 发起人类 - 使用内部类作为备忘录
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;}}
}
使用内部类的管理者类:
// 管理者类
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;}
}
测试内部类实现:
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提供的序列化机制可以看作是备忘录模式的一种实现形式,它允许我们将对象的状态保存到文件中,并在需要时恢复。
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;}}
}
测试序列化实现:
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
类提供了撤销/重做功能,这是备忘录模式的另一种实现。
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 备忘录模式与命令模式
备忘录模式经常与命令模式结合使用,特别是在需要撤销操作的场景中。命令模式封装操作,而备忘录模式保存状态。
// 命令接口
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;}@Overridepublic void execute() {memento = calculator.createMemento(); // 保存当前状态calculator.add(valueToAdd);}@Overridepublic 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;}@Overridepublic void execute() {memento = calculator.createMemento(); // 保存当前状态calculator.subtract(valueToSubtract);}@Overridepublic 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 如何减少备忘录模式的内存消耗?
问题:在一些应用场景中,备忘录可能会占用大量内存资源,尤其是当需要保存大量状态或对象状态非常大时。
解决方案:
- 增量备忘录:只保存状态的变化部分,而不是完整状态。
- 压缩备忘录:对备忘录中的数据进行压缩存储。
- 惰性保存:根据实际需要保存状态,而不是定期保存。
- 限制历史记录数量:设置一个最大历史记录数,超出时删除最旧的记录。
- 使用外部存储:将备忘录保存到文件或数据库,而不是内存中。
// 增量备忘录示例
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 如何实现多层撤销/重做?
问题:简单的备忘录模式通常只提供单一的撤销/恢复功能,但实际应用可能需要多层次的撤销/重做支持。
解决方案:
- 使用两个栈:分别用于撤销和重做操作。
- 记录操作序列:保存完整的操作序列,支持在任意点恢复。
- 命令模式结合:使用命令模式记录操作,备忘录模式记录状态。
// 多层撤销/重做管理器
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的序列化机制来保存和恢复复杂对象状态。
- 选择性备忘录:只保存关键的状态信息,而不是所有状态。
- 分层备忘录:为对象的不同层次创建不同的备忘录。
// 组合备忘录示例
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 设计建议
-
轻量化备忘录:尽量只保存必要的状态信息,减小备忘录的大小。
-
控制备忘录数量:设置合理的备忘录保存策略,避免过多备忘录占用资源。
-
结合其他模式:考虑与命令模式、原型模式等结合使用,解决更复杂的问题。
-
注意性能问题:在频繁创建或恢复备忘录的场景中,注意性能优化。
-
保护隐私数据:确保备忘录中的敏感数据不会被外部访问。
备忘录模式虽然概念简单,但应用灵活,是实现状态管理、操作撤销等功能的有力工具。通过合理使用备忘录模式,我们可以使应用程序更加健壮,提供更好的用户体验。