简介
Without violating encapsulation, capture and externalize an object's internal state so that the object can be restored to this state later.
在不破坏封装的前提下,捕获一个对象的内部状态,并在对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
备忘录模式(Memento Pattern)又称为 快照模式(Snapshot Pattern) 或 Token模式。
在软件系统中,备忘录模式 为我们提供一种 “后悔药” 的机制,它通过存储系统各个历史状态的快照,使得我们可以在任一时刻将系统回滚到某一个历史状态。
对于 备忘录模式 来说,比较贴切的现实场景应该是游戏的存档功能,通过将游戏当前进度存储到本地文件系统或数据库中,使得下次继续游戏时,玩家可以从之前的位置继续进行。
备忘录模式 本质:从发起人实体类(Originator)隔离存储功能,降低实体类的职责。同时由于存储信息(Memento)独立,且存储信息的实体交由管理类(Caretaker)管理,则可以通过为管理类扩展额外的功能对存储信息进行扩展操作(比如增加历史快照功能···)。
主要解决
存储实体(Originator)状态,存储历史快照,回滚历史状态。
优缺点
优点
- 简化发起人实体类(Originator)职责,隔离状态存储与获取,实现了信息的封装,客户端无需关心状态的保存细节;
- 提供状态回滚功能;
缺点
- 消耗资源:如果需要保存的状态过多时,每一次保存都会消耗很多内存;
使用场景
- 需要保存历史快照的场景;
- 希望在对象之外保存状态,且除了自己其他类对象无法访问状态保存具体内容;
模式讲解
首先来看下 备忘录模式 的通用 UML 类图:
从 UML 类图中,我们可以看到,备忘录模式 主要包含三种角色:
- 发起人角色(Originator):负责创建一个备忘录,记录自身需要保存的状态;具备状态回滚功能;
- 备忘录角色(Memento):用于存储 Originator 的内部状态,且可以防止 Originator 以外的对象进行访问;
- 备忘录管理员角色(Caretaker):负责存储,提供,管理备忘录(Memento),无法对备忘录内容进行操作和访问;
以下是 备忘录模式 的通用代码:
class Client {
public static void main(String[] args) {
// 来一个发起人
Originator originator = new Originator();
// 来一个备忘录管理员
Caretaker caretaker = new Caretaker();
// 管理员存储发起人的备忘录
caretaker.storeMemento(originator.createMemento());
// 发起人从管理员获取备忘录进行回滚
originator.restoreMemento(caretaker.getMemento());
}
// 备忘录
static class Memento {
private String state;
public Memento(String state){
this.state = state;
}
public String getState() {
return this.state;
}
public void setState(String state) {
this.state = state;
}
}
// 备忘录管理员
static class Caretaker {
// 备忘录对象
private Memento memento;
public Memento getMemento() {
return this.memento;
}
public void storeMemento(Memento memento) {
this.memento = memento;
}
}
// 发起人
static class Originator {
// 内部状态
private String state;
public String getState() {
return this.state;
}
public void setState(String state) {
this.state = state;
}
// 创建一个备忘录
public Memento createMemento() {
return new Memento(this.state);
}
// 从备忘录恢复
public void restoreMemento(Memento memento) {
this.setState(memento.getState());
}
}
}
注:备忘录模式 要求备忘录(Memento)只对发起人(Originator)内容可见,对其他对象(Caretaker)内容不可见;但是在上面代码中,备忘录管理员(Caretaker)其实是可以通过备忘录(Memento)提供的相关方法(getState
,setState
)获取和修改内部状态,这违背了 备忘录模式 的要求,可能造成备忘录内部状态无意间被其他对象修改,导致发起人状态恢复错误,系统稳定性下降。
修复上述问题的代码如下所示:
interface IMemento {
}
static class Caretaker {
// 备忘录对象
private IMemento memento;
public IMemento getMemento() {
return this.memento;
}
public void storeMemento(IMemento memento) {
this.memento = memento;
}
}
static class Originator {
// 内部状态
private String state;
...
...
// 从备忘录恢复
public void restoreMemento(IMemento memento) {
this.state = ((Memento) memento).state;
}
private static class Memento implements IMemento {
private String state;
private Memento(String state) {
this.state = state;
}
}
}
从上面的代码中我们可以看到:我们把Memento
作为Originator
的私有静态内部类,这样就满足了Originator
对Memento
具有宽访问权限,但是直接这样做,其他类(Caretaker
)是无法访问Memento
的,因此在最上面我们通过定义一个空接口IMemento
,然后让Memento
实现IMemento
,变相将Memento
扩展为公有IMemento
,这样就实现了其他类(Caretaker
)对Memento
具备窄访问权限。这种形式才算是严格满足 备忘录模式 的设计要求。
举个例子
例子:假设现有一个游戏,该游戏规定,对于之前通关的关卡,可以随时跳回到任一关卡继续游戏。也就是说,如果你现在已经在第30关卡,那么30关卡之前的任一一关,你可以随时切回去玩。请用代码实现上述游戏逻辑。
分析:对于已通过的关卡,可以随时切换回去玩,那么系统肯定是对已通过的关卡做了备份,对于通关的每一关卡的相关内容都进行了存储(为了简单,我们只认为对关卡名字,关卡通关时间做了备份),那么我们只需实现存储功能与回滚功能即可。
具体代码如下:
class Client {
public static void main(String[] args) {
GameCaretaker caretaker = new GameCaretaker();
Game game = new Game();
System.out.println(game);
caretaker.addSnapshot(game.createGameInfo());
game.doneChapter("002", 20);
System.out.println(game);
caretaker.addSnapshot(game.createGameInfo());
game.doneChapter("003", 30);
System.out.println(game);
caretaker.addSnapshot(game.createGameInfo());
game.restore(caretaker.getSnapshot("002"));
System.out.println("rollback to chapter 002");
System.out.println(game);
}
// 接口:IMemento
interface IGameInfo {
// 返回关卡名称
String name();
}
// Caretaker
static class GameCaretaker {
private Map<String, IGameInfo> mGameSnapshots = new HashMap<>();
public void addSnapshot(IGameInfo snapshot) {
this.mGameSnapshots.put(snapshot.name(), snapshot);
}
public IGameInfo getSnapshot(String name) {
return this.mGameSnapshots.getOrDefault(name, null);
}
}
// Originator
static class Game {
// 关卡时间
private String mName;
// 通关时间
private int mCost;
public Game() {
this.mName = "001";
this.mCost = 10;
}
public Game(String name, int cost) {
this.mName = name;
this.mCost = cost;
}
public void doneChapter(String name, int cost) {
this.setName(name);
this.setCost(cost);
}
private void setName(String name) {
this.mName = name;
}
private void setCost(int cost) {
this.mCost = cost;
}
public IGameInfo createGameInfo() {
return new GameInfoStore(this.mName, this.mCost);
}
public void restore(IGameInfo info) {
if (info == null) {
throw new IllegalArgumentException("Game Snapshot is empty!!");
}
GameInfoStore game = (GameInfoStore) info;
this.mName = game.name;
this.mCost = game.cost;
}
@Override
public String toString() {
return String.format("Game[%s] cost you: %d mins", this.mName, this.mCost);
}
// Memento
private static class GameInfoStore implements IGameInfo {
private String name;
private int cost;
private GameInfoStore(String name, int cost) {
this.name = name;
this.cost = cost;
}
@Override
public String name() {
return this.name;
}
}
}
}
我们通过为GameCaretaker
添加一个集合记录游戏关卡各个记录,使整个系统具备回滚功能。
运行结果如下:
Game[001] cost you: 10 mins
Game[002] cost you: 20 mins
Game[003] cost you: 30 mins
rollback to chapter 002
Game[002] cost you: 20 mins