18.如果再回到从前 - 备忘录模式(Memento Pattern)

18.1 如果再给我一次机会

  • 时间: 5月6日18点   地点: 小菜大鸟住所的客厅   人物: 小菜, 大鸟

     “小菜, 今天上午看NBA了吗? 火箭季后赛第七场对爵士的比赛.” 大鸟问道.

     “没有, 不过结果倒是在网上第一时间就知道了. 在最后比赛剩下4 分钟时, 比分还相同, 到剩下57.8秒的时候, 火箭也只是落后2分, 可惜, 最后的两个进攻篮板球没有抢到, 火箭就输掉了比赛.”
     “是呀, 最后一分钟的失误, 几乎就等于输掉了整个赛季.”
     “如果火箭任何一人能抓到最后篮板中的一个, 结果完全可能不是这样. 真是遗憾啊.” 小菜感慨道.
     “很多时候我们做了件事之后, 却开始后悔了. 这就是人类的内心脆弱的一面. 时间不能倒流, 不过怎么样人生是无法回到从前的, 但是软件就不一样了. 还记得以前玩儿一些单机pc 游戏的时候吗, 通常我都是在打大 boss 之前, 先保存一个进度, 然后如果通关失败了, 我可以再返回刚才那个进度在恢复原来的状态, 从头来过. 从这点上来说, 我们都比姚明强.”
     “哈, 这其中原理是不是就是把当年的游戏状态的各种参数储存, 以便恢复的时候读取呢?”
     “是的, 通常这种保存都是存在磁盘上了, 以便日后读取. 但对于一些更为常规的应有, 比如我们下期是需要悔棋, 编写玩当时需要撤销, 查看网页时需要后退, 这些相对频繁的操作并不需要保存在磁盘中, 只要将保存在内存中的状态回复一下即可.”
     “嗯, 这是更普通的应用, 很多开发中都会用到.”
     “那我简单地说个场景, 你想想看怎么用代码实现. 游戏的某个场景, 一游戏角色有生命力, 攻击力, 防御力等等数据, 在打 boss 前和后一定会不一样的, 我们允许玩家如果感觉与 Boss 决斗的效果不理想可以让游戏回复到决斗前.”
     “好的, 我试试看.”

18.2 游戏存进度

     游戏角色类, 又来存储角色的生命力, 攻击力, 防御力的数据.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class GameRole {
// 生命力
private int vit;
// 攻击力
private int atk;
// 防御力
private int def;
public int getVit() { return vit; }
public void setVit(int vit) { this.vit = vit; }
public int getAtk() { return atk; }
public void setAtk(int atk) { this.atk = atk; }
public int getDef() { return def; }
public void setDef(int def) { this.def = def; }
// 状态显示
public void stateDisplay() {
System.out.println("角色当前状态: ");
System.out.println("体力: " + this.vit);
System.out.println("攻击力: " + this.atk);
System.out.println("防御力: " + this.def);
System.out.println("");
}
// 获得初始状态
public void getInitState() {
// 数据通常来自本机磁盘或远程数据库
this.vit = 100;
this.atk = 100;
this.def = 100;
}
// 战斗
public void fight() {
// 在于Boss 大战后游戏数据损耗为0
this.vit = 0;
this.atk = 0;
this.def = 0;
}
}

     客户端调用时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
// 大战 boss 前, 获得角色的初始状态
GameRole lixiaoyao = new GameRole();
lixiaoyao.getInitState();
lixiaoyao.stateDisplay();
// 保存进度
GameRole backup = new GameRole();
backup.setVit(lixiaoyao.getVit());
backup.setAtk(lixiaoyao.getAtk());
backup.setDef(lixiaoyao.getDef());
// 大战 boss 时, 损耗严重
lixiaoyao.fight();
lixiaoyao.stateDisplay();
// 回复之前的状态
lixiaoyao.setVit(backup.getVit());
lixiaoyao.setAtk(backup.getAtk());
lixiaoyao.setDef(backup.getDef());
lixiaoyao.stateDisplay();
}

     结果显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
角色当前状态:
体力: 100
攻击力: 100
防御力: 100
角色当前状态:
体力: 0
攻击力: 0
防御力: 0
角色当前状态:
体力: 100
攻击力: 100
防御力: 100

     “小菜, 这样的写法, 确实是实现了我的要求, 但是问题也确实多多.”
     “哈, 你的经典结论, 代码无错未必无忧. 说吧, 我有心理准备.”
     “问题主要在于客户端这里的应用. 下面这一段代码有问题, 因为这样写就把整个游戏角色的细节暴露给了客户端, 你的客户端的职责就太大了, 需要知道角色的生命力, 攻击力, 防御力这些细节, 还要对她进行 ‘备份’. 以后需要增加新的数据, 例如增加 ‘魔法力’ 或修改现有的某种力, 例如 ‘生命力’ 改为 ‘经验值’, 这部分就一定要修改了. 同样的道理也存在于回复时的代码.”

1
2
3
4
5
// 暴露了实现细节, 不足取
GameRole backup = new GameRole();
backup.setVit(lixiaoyao.getVit());
backup.setAtk(lixiaoyao.getAtk());
backup.setDef(lixiaoyao.getDef());

1
2
3
4
// 暴露了实现细节, 不足取
lixiaoyao.setVit(backup.getVit());
lixiaoyao.setAtk(backup.getAtk());
lixiaoyao.setDef(backup.getDef());

     “显然, 我们希望的是把这些 ‘游戏角色’ 的存取状态细节封装起来, 而且最好是封装在外部类当中. 以体现这则分离.”

18.3 备忘录模式

     “所以我们需要学习一个新模式, 备忘录模式.”

备忘录(Memento): 在不破坏封装性的前提下, 捕获一个对象的内部状态, 并在该对象之外保存这个状态. 这样以后就可将该对象回到原先保存的状态.



     Originator(发起人): 负责创建一个备忘录 Memento, 用以记录当前时刻它的内部状态, 并可使用备忘录回复内部状态. Originator 可根据需要决定 Memento 存储 Originator 的那些内部状态.
     Memento(备忘录): 负责存储 Originator 对象的内部状态, 并可防止 Originator 以外的其他对象访问备忘录 Memento. 备忘录有两个接口, Caretaker 只能看到备忘录的窄接口, 它只能将备忘录传递到其他对象. Originator 能够看到一个宽接口, 允许它访问返回到先前状态所需的所有数据.
     Caretaker(管理者): 负责保存号备忘录 Memento, 不能对备忘录的内容进行操作或检查.
     “就刚才的例子, ‘游戏角色’ 类其实就是一个 Originator, 而你用了同样的 ‘游戏角色’ 实例 ‘备份’ 来做备忘录, 这在当需要保存全部信息时, 是可以考虑的, 而用clone 的方式来实现 Memento 的状态保存可能是更好的办法, 但是如果这样的话, 使得我们相当于对上层应用开放了 Originator 的全部接口. 这对于备份的时候是不合适.”
     “那如果我们不需要保存全部的信息以备使用时, 怎么办?”
     “哈, 对的, 这或许是更多可能发生的情况, 我们需要保存的并不是全部信息, 而只是部分, 那么就应该有一个独立的备忘录类 Memento, 他只拥有需要保存的信息的属性.”

18.4 备忘录模式基本代码

     发起人(Originator) 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Originator {
private String state; // 需要保存的属性, 可能有多个
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
// 创建备忘录, 将当前需要保存的信息导入
// 并实例化出一个对象
public Memento createMemento() {
return new Memento(state);
}
// 回复备忘录
// 将 Memento 导入并将相关数据回复
public void setMemento(Memento memento) {
state = memento.getState();
}
public void show() {
// 显示数据
System.out.println("state: " + state);
}
}

     备忘录(Memento) 类

1
2
3
4
5
6
7
8
9
10
11
public class Memento {
private String state;
public Memento(String state) {
this.state = state;
}
public String getState() {
return state;
}
}

     管理者(caretaker) 类

1
2
3
4
5
6
7
8
9
10
11
public class Caretaker {
private Memento memento;
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}

     客户端程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
// Originator 初始状态, 状态属性为'On'
Originator o = new Originator();
o.setState("ON");
o.show();
// 保存状态时, 由于有了很好的封装,
// 可以隐藏 Originator 的实现细节
Caretaker c = new Caretaker();
c.setMemento(o.createMemento());
// Originator 改变了状态属性为 'Off'
o.setState("off");
o.show();
// 回复原始状态
o.setMemento(c.getMemento());
o.show();
}

     “哈, 我明白了, 这当中就是把要保存的的细节封装在的 Memento 中了, 那一天要更改保存的细节也不用影响客户端了. 那么这个备忘录模式都用在一些生么样的场合呢?”
     “Memento 模式比较适用于功能比较复杂, 大需要维护或记录历史属性的类, 或者需要保存的属性只是众多属性的一小部分时, Originator 可以根据保存的 Memento 信息还原到前一状态.
     “我记得好像命令模式也有实现类似撤销的作用?”
     “哈, 小子记性不错, 如果在某个系统中使用命令模式时, 需要实现命令的撤销功能, 那么命令模式可以使用备忘录模式来存储可撤销操作的状态[DP]. 有时一些对象的内部信息必须保存在对象以外的地方, 但是必须要有对象自己读取, 这时, 使用备忘录可以把复杂的对象内部信息对其他的对象屏蔽起来[DP], 从而可以恰当地保持封装的边界.”
     “我感觉可能最大的作用是在当角色的状态改变的时候, 有可能这个状态无效, 这时候就可以使用暂时存储起来的备忘录模式将状态恢复[DP]这个作用吧?”
     “说得好, 这当然是最重要的作用了.”
     “明白, 我学会了.”
     “别急, 你还没有把刚才的代码改成备忘录模式呢.”
     “啊, 你就不打算饶过我. 等着, 看我来拿满分.”

18.5 游戏进度备忘



     游戏角色类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class GameRole {
//...
// 保存角色状态
public RoleStateMemento SaveState() {
return new RoleStateMemento(vit, atk, def);
}
// 恢复角色状态
public void recoveryState(RoleStateMemento memento) {
this.vit = memento.getVit();
this.atk = memento.getAtk();
this.def = memento.getDef();
}
// .....
}

     角色状态存储箱类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class RoleStateMemento {
private int vit;
private int atk;
private int def;
public RoleStateMemento(int vit, int atk, int def) {
this.vit = vit;
this.atk = atk;
this.def = def;
}
public int getVit() { return vit; }
public void setVit(int vit) { this.vit = vit; }
public int getAtk() { return atk; }
public void setAtk(int atk) { this.atk = atk; }
public int getDef() { return def; }
public void setDef(int def) { this.def = def; }
}

     角色状态管理者类

1
2
3
4
5
6
7
8
9
10
11
public class RoleStateCaretaker {
private RoleStateMemento memento;
public RoleStateMemento getMemento() {
return memento;
}
public void setMemento(RoleStateMemento memento) {
this.memento = memento;
}
}

     客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) {
// 大战 boss 前, 获得角色的初始状态
GameRole lixiaoyao = new GameRole();
lixiaoyao.getInitState();
lixiaoyao.stateDisplay();
// 保存进度
RoleStateCaretaker stateAdmin = new RoleStateCaretaker();
stateAdmin.setMemento(lixiaoyao.SaveState());
// 大战 boss 时, 损耗严重
lixiaoyao.fight();
lixiaoyao.stateDisplay();
// 回复之前的状态
lixiaoyao.recoveryState(stateAdmin.getMemento());
lixiaoyao.stateDisplay();
}

     “看看, 能不能得满分, 我查了好几遍.”
     “不错, 写的还行. 你要注意, 备忘录模式也是有缺点的, 角色状态需要完全存储到备忘录对象中, 如果状态数据很大很多, 那么在资源消耗上, 备忘录对象以非常耗内存.”
     “恩, 明白. 所以也不是用的越多越好.”

~感谢捧场,您的支持将鼓励我继续创作~