备忘录模式
定义
备忘录模式提供了一种对象状态的撤销实现机制,当系统中的某一个对象需要恢复到某一历史状态时可以使用备忘录模式进行设计。

提示
很多软件都提供了 撤销(Undo)操作,如 Word 、记事本、Photoshop、IDEA 等软件在编辑时按 Ctrl+Z 组合键时能撤销当前操作,使文档恢复到之前的状态;还有在浏览器中的后退键、数据库事务管理中的回滚操作、玩游戏时的中间结果存档功能、数据库与操作系统的备份操作、棋牌类游戏中的悔棋功能等属于这类。
备忘录模式(memento pattern):在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。
模式原理

- Caretaker(看护人):对备忘录进行管理,有备忘录对象引用
- Memento(备忘录角色):存储发起人的内部状态,在需要的时候把状态提供给发起人。
- Originator(发起人):要被记录的对象,提供了创建备忘录和恢复备忘录的方法。
核心类实现
1. 备忘录角色(Memento)
/**
* 备忘录角色
* 访问权限为:默认,在同包下可见(保证只有发起者类可以访问备忘录类)
*/
class Memento {
private String state = "从备份对象恢复原始对象";
private String id;
private String name;
private String phone;
public Memento() {
}
public Memento(String id, String name, String phone) {
this.id = id;
this.name = name;
this.phone = phone;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
@Override
public String toString() {
return "Memento{" +
"state='" + state + '\'' +
", id='" + id + '\'' +
", name='" + name + '\'' +
", phone='" + phone + '\'' +
'}';
}
}
2. 发起人角色(Originator)
/**
* 发起人角色
*/
public class Originator {
private String state = "原始对象";
private String id;
private String name;
private String phone;
// 创建备忘录对象
public Memento createMemento() {
return new Memento(id, name, phone);
}
// 恢复对象
public void restoreMemento(Memento memento) {
this.state = memento.getState();
this.id = memento.getId();
this.name = memento.getName();
this.phone = memento.getPhone();
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
@Override
public String toString() {
return "Originator{" +
"state='" + state + '\'' +
", id='" + id + '\'' +
", name='" + name + '\'' +
", phone='" + phone + '\'' +
'}';
}
}
3. 负责人类(Caretaker)
/**
* 负责人类:获取和保存备忘录对象
*/
public class Caretaker {
private Memento memento;
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}
4. 客户端测试(Client)
public class Client {
public static void main(String[] args) {
// 创建发起人对象
Originator o1 = new Originator();
o1.setId("1");
o1.setName("tom");
o1.setPhone("13182678111");
System.out.println("=========" + o1);
// 创建负责人对象
Caretaker caretaker = new Caretaker();
caretaker.setMemento(o1.createMemento());
// 修改
o1.setName("update");
System.out.println("=========" + o1);
// 从负责人对象中获取备忘录对象,实现恢复操作
o1.restoreMemento(caretaker.getMemento());
System.out.println("=========" + o1);
}
}
运行结果示例
=========Originator{state='原始对象', id='1', name='tom', phone='13182678111'}
=========Originator{state='原始对象', id='1', name='update', phone='13182678111'}
=========Originator{state='从备份对象恢复原始对象', id='1', name='tom', phone='13182678111'}
设计要点
- 封装性保护:
Memento类使用默认访问权限,确保只有同包下的Originator类可以访问 - 状态分离:将对象状态保存在独立的备忘录对象中,实现状态与对象的分离
- 职责明确:每个角色职责单一,
Originator负责状态管理,Caretaker负责备忘录管理 - 恢复机制:通过
restoreMemento方法实现状态的恢复操作
应用实例
设计一个收集水果和获取金钱数的掷骰子游戏,游戏规则如下:
- 游戏玩家通过扔骰子来决定下一个状态。
- 当点数为 1 , 玩家金钱增加。
- 当点数为 2 , 玩家金钱减少。
- 当点数为 6 ,玩家会得到水果。
- 当钱消耗到一定程序,就恢复到初始状态。
游戏实现
1. 备忘录类(Memento)
import java.util.ArrayList;
import java.util.List;
/**
* 表示玩家的状态
*/
class Memento {
// 玩家获取的金币
private int money;
// 玩家获取的水果
private ArrayList<String> fruits;
public Memento(int money) {
this.money = money;
this.fruits = new ArrayList<>();
}
// 获取当前玩家的金币
int getMoney() {
return money;
}
void setMoney(int money) {
this.money = money;
}
// 获取当前玩家的水果
List<String> getFruits() {
return (List<String>) fruits.clone();
}
void addFruit(String fruit) {
fruits.add(fruit);
}
}
2. 玩家类(Player)
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
* 玩家类
*/
public class Player {
// 金币
private int money;
// 玩家获得的水果
private List<String> fruits;
private static String[] fruitsName = {"苹果", "葡萄", "香蕉", "橘子"};
Random random = new Random();
public Player(int money) {
this.money = money;
this.fruits = new ArrayList<>();
}
// 获取当前所有金币
public int getMoney() {
return money;
}
// 获取一个水果
public String getFruit() {
String prefix = "";
if (random.nextBoolean()) {
prefix = "好吃的";
}
// 从数组中拿一个水果
String f = fruitsName[random.nextInt(fruitsName.length)];
return prefix + f;
}
// 掷骰子方法
public void yacht() {
int dice = random.nextInt(6) + 1; // 掷骰子操作
if (dice == 1) {
money += 100;
System.out.println("所持有的金币增加了");
} else if (dice == 2) {
money /= 2;
System.out.println("所持有的金币减少一半");
} else if (dice == 6) {
String fruit = getFruit();
System.out.println("获取了水果:" + fruit);
fruits.add(fruit);
} else {
System.out.println("无效数字,请重新投掷");
}
}
// 拍摄快照
public Memento createMemento() {
Memento memento = new Memento(money);
for (String fruit : fruits) {
if (fruit.startsWith("好吃的")) {
memento.addFruit(fruit);
}
}
return memento;
}
// 撤销方法
public void restoreMemento(Memento m) {
this.money = m.getMoney();
this.fruits = m.getFruits();
}
@Override
public String toString() {
return "Player{" +
"fruits=" + fruits +
", money=" + money +
'}';
}
}
3. 主应用程序(MainApp)
public class MainApp {
public static void main(String[] args) throws InterruptedException {
// 创建玩家类,设置初始金币
Player player = new Player(100);
// 创建备忘录对象
Memento memento = player.createMemento();
for (int i = 0; i < 100; i++) {
// 显示扔骰子的次数
System.out.println("第" + i + "次投掷!");
// 显示当前玩家状态
System.out.println("当前状态:" + player);
// 开启游戏
player.yacht();
System.out.println("玩家所持有的金币:" + player.getMoney());
if (player.getMoney() > memento.getMoney()) {
System.out.println("赚到金币,保存当前状态,继续游戏!");
memento = player.createMemento(); // 更新快照
} else if (player.getMoney() < memento.getMoney() / 2) {
System.out.println("所持的金币不多,将游戏恢复到初始状态");
player.restoreMemento(memento);
}
Thread.sleep(1000);
}
}
}
游戏运行示例
第0次投掷!
当前状态:Player{fruits=[], money=100}
获取了水果:好吃的苹果
玩家所持有的金币:100
赚到金币,保存当前状态,继续游戏!
第1次投掷!
当前状态:Player{fruits=[好吃的苹果], money=100}
所持有的金币增加了
玩家所持有的金币:200
赚到金币,保存当前状态,继续游戏!
第2次投掷!
当前状态:Player{fruits=[好吃的苹果], money=200}
所持有的金币减少一半
玩家所持有的金币:100
所持的金币不多,将游戏恢复到初始状态
第3次投掷!
当前状态:Player{fruits=[好吃的苹果], money=100}
无效数字,请重新投掷
玩家所持有的金币:100
设计特点分析
1. 状态管理策略
- 自动保存:当玩家赚到金币时,自动保存当前状态
- 智能恢复:当金币减少到一半时,自动恢复到之前保存的状态
- 选择性保存:只保存"好吃的"水果,过滤掉普通水果
2. 备忘录模式应用
- 状态封装:玩家状态(金币、水果)封装在
Memento对象中 - 状态恢复:通过
restoreMemento方法实现状态回滚 - 状态分离:游戏逻辑与状态保存逻辑分离
3. 游戏机制设计
- 随机性:通过掷骰子引入随机性,增加游戏趣味性
- 风险控制:当金币损失过多时,自动恢复到安全状态
- 奖励机制:通过水果收集和金币增加提供正向反馈
4. 代码优化点
- 深拷贝:使用
clone()方法避免水果列表被外部修改 - 状态检查:通过比较当前金币与保存金币来决定是否保存或恢复
- 用户体验:添加延时和状态显示,提升游戏体验
实际应用价值
这个游戏示例展示了备忘录模式在实际应用中的价值:
- 游戏存档系统:类似游戏中的存档/读档功能
- 状态回滚:当操作失误时可以回到之前的状态
- 数据保护:通过备忘录保护重要数据不被意外修改
- 用户体验:提供撤销功能,提升用户操作体验
总结
优点
- 提供状态恢复机制:提供了一种状态恢复的实现机制,使得用户可以方便的回到一个特定的历史步骤,当新的状态无效或者存在问题的时候,可以使用暂时存储起来的备忘录,将状态恢复。
- 信息封装保护:备忘录实现了对信息的封装,一个备忘录对象是一种发起者对象状态的表示,不会被其它代码所改动,备忘录保存了发起者的状态,采用集合来存储备忘录可以实现多次撤销的操作。
- 支持多次撤销:通过保存多个备忘录对象,可以实现多次撤销操作,提供更灵活的状态管理。
- 不破坏封装性:在保存和恢复状态的过程中,不会暴露对象的内部实现细节。
缺点
- 资源消耗过大:如果需要保存的发起者类的成员变量比较多,就不可避免的需要占用大量的存储空间,每保存一次对象的状态,都需要消耗一定的系统资源。
- 状态同步问题:当对象状态发生变化时,需要及时更新备忘录,否则可能导致状态不一致。
- 内存泄漏风险:如果备忘录对象没有被及时清理,可能导致内存泄漏。
- 性能影响:频繁的状态保存和恢复操作可能影响系统性能。
使用场景
- 需要保存一个对象在某一时刻的状态时:可以使用备忘录模式,如游戏存档、文档编辑的撤销功能。
- 不希望外界直接访问对象内部状态时:通过备忘录对象来间接访问对象状态,保护对象的封装性。
- 需要实现撤销功能时:如文本编辑器、图形编辑器等需要撤销操作的应用。
- 需要实现事务回滚时:如数据库事务管理、文件操作等需要回滚功能的场景。
- 需要实现状态快照时:如虚拟机快照、系统备份等需要保存系统状态的场景。
最佳实践
- 合理控制备忘录数量:避免保存过多的备忘录对象,可以通过限制数量或定期清理来管理内存使用。
- 使用深拷贝:在创建备忘录时使用深拷贝,避免对象引用导致的状态污染。
- 及时清理资源:当备忘录不再需要时,及时清理相关资源,避免内存泄漏。
- 考虑性能影响:在频繁操作的对象上使用备忘录模式时,需要考虑性能影响,必要时可以优化保存策略。
- 文档化备忘录结构:明确记录备忘录对象的结构和用途,便于后续维护和理 解。
与其他模式的关系
- 与命令模式结合:可以将备忘录模式与命令模式结合使用,实现更复杂的撤销/重做功能。
- 与状态模式结合:备忘录模式可以与状态模式结合,保存对象在不同状态下的快照。
- 与原型模式结合:可以使用原型模式来创建备忘录对象的副本,提高性能。