引子
小帅在一家游戏公司工作。最近领导让他设计游戏的自动存档功能,每次玩家要挑战boss的时候,系统要能够实现自动存档。如果玩家挑战失败,game over 了,还能恢复到挑战之前的状态再来一次。
小帅心想,我用一个备份对象把所有的游戏参数记录下来,当玩家要读取存档的时候,再把备份对象里的数据恢复回来不就行了吗?
普通方法
小帅很快就写出了代码:
/**
* 英雄类
*/
public class Hero {
/**
* 生命值
*/
private int healthPoint;
/**
* 魔法值
*/
private int magicalValue;
/**
* 攻击力
*/
private int attackPower;
public Hero(int healthPoint, int magicalValue, int attackPower) {
this.healthPoint = healthPoint;
this.magicalValue = magicalValue;
this.attackPower = attackPower;
}
/**
* 游戏结束
*/
public void gameOver() {
this.healthPoint = 0;
this.magicalValue = 0;
this.attackPower = 0;
}
/**
* 设置属性
* @param healthPoint
* @param magicalValue
* @param attackPower
*/
public void setState(int healthPoint, int magicalValue, int attackPower) {
this.healthPoint = healthPoint;
this.magicalValue = magicalValue;
this.attackPower = attackPower;
}
@Override
public String toString() {
StringBuffer display = new StringBuffer();
display.append("生命值:" + this.healthPoint + "\n");
display.append("魔法值:" + this.magicalValue + "\n");
display.append("攻击力:" + this.attackPower + "\n");
return display.toString();
}
public int getHealthPoint() {
return healthPoint;
}
public void setHealthPoint(int healthPoint) {
this.healthPoint = healthPoint;
}
public int getMagicalValue() {
return magicalValue;
}
public void setMagicalValue(int magicalValue) {
this.magicalValue = magicalValue;
}
public int getAttackPower() {
return attackPower;
}
public void setAttackPower(int attackPower) {
this.attackPower = attackPower;
}
}
/**
* 客户端类
*/
public class Client {
public static void main(String[] args) {
Hero hero = new Hero(90,85,70);
// 挑战boss之前的状态
System.out.println("挑战boss之前的状态:\n" + hero);
// 保存进度
Hero heroBackUp = new Hero(hero.getHealthPoint(), hero.getMagicalValue(), hero.getAttackPower());
// 挑战失败
hero.gameOver();
System.out.println("挑战失败后的状态:\n" + hero);
// 恢复进度
hero.setState(heroBackUp.getHealthPoint(), heroBackUp.getMagicalValue(), heroBackUp.getAttackPower());
System.out.println("恢复进度后的状态:\n" + hero);
}
}
输出:
挑战boss之前的状态:
生命值:90
魔法值:85
攻击力:70
挑战失败后的状态:
生命值:0
魔法值:0
攻击力:0
恢复进度后的状态:
生命值:90
魔法值:85
攻击力:70
小帅觉得这不是很简单吗?
我只要再建个heroBackUp对象,把hero对象的状态保存进去,等需要读档的时候再读取heroBackUp对象中的状态不就行了吗?
这时候项目组里的老王发话了:
你直接用Hero类的对象作为备份,方便是方便,但是不安全,Hero类中有属性公有的set方法。备份的heroBackUp对象,有可能会被别人调用set方法更改属性。
备份对象是只读的,不能被修改。
heroBackUp对象可能会被其他对象修改属性,这违背了封装原则。
那如何才能在不违背封装原则的前提下,备份一个对象呢?小帅连忙问道。
老王微微一笑:这就要用到备忘录模式了。
备忘录模式
备忘录模式:在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
备忘录模式是一种行为设计模式, 允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。
- Memento(备忘录):备忘录存储原发器对象的内部状态;备忘录的内部状态只能由原发器访问。
- Originator(原发器):创建备忘录,记录当前的状态;使用备忘录恢复状态。
- Caretaker(负责人):管理备忘录;不能对备忘录的内容进行操作或检查。
老王没多久就把代码改造好了:
Originator类:
/**
* 英雄类(就是Originator)
*/
public class Hero {
/**
* 生命值
*/
private int healthPoint;
/**
* 魔法值
*/
private int magicalValue;
/**
* 攻击力
*/
private int attackPower;
public Hero(int healthPoint, int magicalValue, int attackPower) {
this.healthPoint = healthPoint;
this.magicalValue = magicalValue;
this.attackPower = attackPower;
}
/**
* 游戏结束
*/
public void gameOver() {
this.healthPoint = 0;
this.magicalValue = 0;
this.attackPower = 0;
}
/**
* 创建备忘录
* @return
*/
public Memento createMemento() {
return new Memento(healthPoint, magicalValue, attackPower);
}
/**
* 从备忘录中恢复数据
* @param memento
*/
public void restoreMemento(Memento memento) {
this.healthPoint = memento.getHealthPoint();
this.magicalValue = memento.getMagicalValue();
this.attackPower = memento.getAttackPower();
}
@Override
public String toString() {
StringBuffer display = new StringBuffer();
display.append("生命值:" + this.healthPoint + "\n");
display.append("魔法值:" + this.magicalValue + "\n");
display.append("攻击力:" + this.attackPower + "\n");
return display.toString();
}
}
Memento :
/**
* 备忘录类
*/
public class Memento {
/**
* 生命值
*/
private int healthPoint;
/**
* 魔法值
*/
private int magicalValue;
/**
* 攻击力
*/
private int attackPower;
public Memento(int healthPoint, int magicalValue, int attackPower) {
this.healthPoint = healthPoint;
this.magicalValue = magicalValue;
this.attackPower = attackPower;
}
/**
* 备忘录中只有get方法,没有set方法,因为备忘录中的数据不应该被修改
* @return
*/
public int getHealthPoint() {
return healthPoint;
}
/**
* 备忘录中只有get方法,没有set方法,因为备忘录中的数据不应该被修改
* @return
*/
public int getMagicalValue() {
return magicalValue;
}
/**
* 备忘录中只有get方法,没有set方法,因为备忘录中的数据不应该被修改
* @return
*/
public int getAttackPower() {
return attackPower;
}
}
Caretaker:
/**
* 负责人类
*/
public class Caretaker {
/**
* 备忘录
*/
private Memento memento;
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}
Client :
/**
* 客户端类
*/
public class Client {
public static void main(String[] args) {
Hero hero = new Hero(90,85,70);
// 挑战boss之前的状态
System.out.println("挑战boss之前的状态:\n" + hero);
// 保存进度
Caretaker caretaker = new Caretaker();
caretaker.setMemento(hero.createMemento());
// 挑战失败
hero.gameOver();
System.out.println("挑战失败后的状态:\n" + hero);
// 恢复进度
hero.restoreMemento(caretaker.getMemento());
System.out.println("恢复进度后的状态:\n" + hero);
}
}
输出:
挑战boss之前的状态:
生命值:90
魔法值:85
攻击力:70
挑战失败后的状态:
生命值:0
魔法值:0
攻击力:0
恢复进度后的状态:
生命值:90
魔法值:85
攻击力:70
我定义一个独立的类(Memento 类)来表示备份,而不是复用 Hero类。这个类只暴露 get() 方法,没有 set() 等任何修改内部状态的方法。这样就保证了数据不会被修改,符合了封装原则。
Caretaker类专门负责管理Memento类,但是Caretaker类对Memento类的权限有限,不能修改Memento类的数据。
老王总结道。
撤销功能的实现
老王接着说:如果我们想实现常见的撤销功能,可以在Caretaker类中用Stack来存储Memento对象。
每进行一次操作就调用Stack的push()方法把一个Memento对象入栈。
撤销的时候就调用Stack的pop()方法出栈,取出一个Memento对象,来恢复状态。
小帅听完不禁赞叹:你可比我家隔壁的老王厉害多了!
总结
当你需要创建对象状态快照来恢复其之前的状态时, 可以使用备忘录模式。
该模式建议将对象状态的副本存储在一个名为备忘录 (Memento) 的特殊对象中。 备忘录让对象自行负责创建其状态的快照,除了创建备忘录的对象外, 任何其他对象都不能访问备忘录的内容。
Caretaker对象必须使用受限接口与备忘录进行交互, 它可以获Memento对象本身, 但它不能获取和更改Memento对象中的属性状态。
优点
- 可以在保持封装状态的情况下,创建对象状态的备忘录,保证了备忘录数据的安全。
- 可以通过让负责人维护备忘录,来简化原发器代码。
缺点
- 如果客户端过于频繁地创建备忘录, 程序将消耗大量内存。