不知道大家小时候有没有玩过经典单机游戏,特别是一些小而美的 RPG 游戏,如金庸群侠传、口袋妖怪等等,由于通关游戏需要很长的时间,所以这些 RPG 游戏都会提供存档的功能,让玩家能够休息一会,明日再战。而下次进入游戏读取存档即可接着上次的画面继续游玩。
游戏中的这种存档、读档功能就可以使用备忘录模式(Memento Pattern)来实现。
1. 定义
备忘录模式是一种行为型设计模式,它的定义是:“在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。”
简而言之就是允许客户端将某个场景恢复到原来保存过的时间点,这个功能跟前段时间的热播电视剧《开端》的不断循环非常像,包括很多年前周星驰的电影《大话西游》中利用月光宝盒不断穿越到紫霞仙子自杀前的时间段,也是同样的道理。
2. 类图
在备忘录模式中,有这样几种角色:
- 发起人(Originator)角色:记录当前对象状态信息,提供创建和恢复备忘录数据的功能
- 备忘录(Memento)角色:负责存储发起人的状态信息,通过备忘录管理者对外暴露
- 备忘录管理者(Caretaker)角色:提供管理备忘录功能(如创建、查询),但不能对备忘录数据进行修改
备忘录模式的类图如下:
3. 示例
本来想用金庸群侠传来作为备忘录模式的场景介绍的,由于金庸群侠传规定输三次就相当于游戏失败,所以每次在挑战高手前我都会存个档,以防直接“退出江湖”。但是 Mac Chorme 浏览器已不提供 flash 能力,而且 4399 小游戏竟然需要实名注册才能玩!
所以今天就用口袋妖怪用精灵球抓精灵的场景来演示备忘录模式,先来说说场景:在口袋妖怪游戏中,最让玩家有成就感的事情莫过于抓捕野生的稀有精灵,但是在某些特殊的场景中,只有一次机会投掷出精灵球,没有成功抓捕到那么精灵就会逃走或者把你的精灵打败,例如:凯西、三神兽等。
假设现在对面的就是凯西(原谅我没有其他素材了,随便抓了只波波,凯西还要打到第二座道馆才能遇到),玩家只有一次机会抓它,此时玩家肯定要先存档,如果这次没成功那么读档重来。
直到成功抓到凯西,这波才能算功成身退。
下面就用备忘录模式来模拟上面的场景,首先创建备忘录角色,这里未被收服的凯西,以及玩家自己的小火龙都是备忘录角色,代码如下:
public class Memento {
// 精灵名称
private String name;
// 精灵生命值
private Integer hp;
// 精灵等级
private String level;
// 精灵归属人
private String belonger;
public Memento(String name, Integer hp, String level, String belonger) {
this.name = name;
this.hp = hp;
this.level = level;
this.belonger = belonger;
}
// 备忘录类内的属性没有 setter 方法,其属性只能在创建备忘录时填入,不应该被外界修改
public String getName() {
return name;
}
public Integer getHp() {
return hp;
}
public String getLevel() {
return level;
}
public String getBelonger() {
return belonger;
}
@Override
public String toString() {
return "Memento{" +
"name='" + name + '\'' +
", hp=" + hp +
", level='" + level + '\'' +
", belonger='" + belonger + '\'' +
'}';
}
}
然后编写备忘录管理者角色,这里与类图有点不一样的是在当前的场景中,存档中会保存多个精灵的状态,所以一个存档会使用一个 List 来存储,同时它会使用一个唯一 key 来保证不重复,其代码如下:
public class Caretaker {
// 由于存档需要保存多个精灵的状态,所以每个存档用一个 list 来存储
private Map<String, List<Memento>> archiveMap = new HashMap<>();
public List<Memento> getArchive(String key) {
return archiveMap.get(key);
}
public void setArchiveMap(String key, List<Memento> archive) {
archiveMap.put(key, archive);
}
}
然后编写发起人角色:
public class Player {
private List<Memento> mementos;
public Player(List<Memento> mementos) {
this.mementos = mementos;
}
public List<Memento> createMemento() {
List<Memento> target = new ArrayList<>();
for (Memento memento : mementos) {
target.add(memento);
}
return target;
}
public void restoreMemento(List<Memento> mementos) {
this.mementos = mementos;
}
@Override
public String toString() {
return "Player{" +
"mementos=" + mementos +
'}';
}
}
然后编写测试代码:
public class Test {
public static void main(String[] args) {
Memento bobo = new Memento("凯西", 10, "Lv3", null);
Memento fireDragon = new Memento("小火龙", 20, "Lv6", "player1");
List<Memento> mementos = new ArrayList<>();
mementos.add(bobo);
mementos.add(fireDragon);
Player player = new Player(mementos);
System.out.println("第一次遇到凯西时的状态: " + player);
System.out.println("存档");
List<Memento> mementoCreated = player.createMemento();
Caretaker caretaker = new Caretaker();
caretaker.setArchiveMap("1", mementoCreated);
System.out.println("抓捕失败,凯西逃走,开始读档");
List<Memento> archive = caretaker.getArchive("1");
player.restoreMemento(archive);
System.out.println("读档后的状态: " + player);
}
}
输出结果为:
第一次遇到凯西时的状态: Player{mementos=[Memento{name='凯西', hp=10, level='Lv3', belonger='null'}, Memento{name='小火龙', hp=20, level='Lv6', belonger='player1'}]}
存档
抓捕失败,凯西逃走,开始读档
读档后的状态: Player{mementos=[Memento{name='凯西', hp=10, level='Lv3', belonger='null'}, Memento{name='小火龙', hp=20, level='Lv6', belonger='player1'}]}
于是,在不断的尝试过后,终于如愿以偿抓到了凯西。
4. 使用场景
备忘录模式的使用场景是:
- 需要保存及恢复数据或者提供回滚功能的场景
5. 小结
本文讲述了备忘录模式,它的定义是——在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
比如一些游戏存档的场景、MySQL MVCC、文本编辑器使用 Ctrl+Z 实现的撤销功能等,都可以使用备忘录模式来实现。
备忘录模式的优点是:
- 提供了数据回滚能力,同时也不会破坏封装性
备忘录模式的缺点是:
- 备忘录模式会存储对象大量的快照版本,如果频繁使用会增加内存消耗
6. 参考资料
- 《设计模式之禅》(第2版)
- 备忘录模式(详解版)
最后,本文收录于个人语雀知识库: 我所理解的后端技术,欢迎来访。
文档信息
- 本文作者:Planeswalker23
- 本文链接:https://planeswalker23.github.io/2022/02/12/%E6%88%91%E6%89%80%E7%90%86%E8%A7%A3%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E7%B3%BB%E5%88%97-%E7%AC%AC19%E7%AF%87-%E6%88%91%E6%89%80%E7%90%86%E8%A7%A3%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E7%B3%BB%E5%88%97-%E7%AC%AC19%E7%AF%87-%E5%9F%BA%E4%BA%8E%E5%A4%87%E5%BF%98%E5%BD%95%E6%A8%A1%E5%BC%8F%E5%AE%9E%E7%8E%B0%E6%B8%B8%E6%88%8F%E5%AD%98%E6%A1%A3%E4%B8%8E%E8%AF%BB%E5%8F%96/
- 版权声明:本作品系原创,作者保留所有权利,未经作者允许,禁止转载和演绎。