簡介
Without violating encapsulation, capture and externalize an object's internal state so that the object can be restored to this state later.
在不破壞封裝的前提下肉盹,捕獲一個對象的內部狀態(tài)沉填,并在對象之外保存這個狀態(tài)之拨。這樣以后就可將該對象恢復到原先保存的狀態(tài)央勒。
備忘錄模式(Memento Pattern)又稱為 快照模式(Snapshot Pattern) 或 Token模式。
在軟件系統(tǒng)中葱跋,備忘錄模式 為我們提供一種 “后悔藥” 的機制持寄,它通過存儲系統(tǒng)各個歷史狀態(tài)的快照,使得我們可以在任一時刻將系統(tǒng)回滾到某一個歷史狀態(tài)年局。
對于 備忘錄模式 來說际看,比較貼切的現(xiàn)實場景應該是游戲的存檔功能,通過將游戲當前進度存儲到本地文件系統(tǒng)或數(shù)據(jù)庫中矢否,使得下次繼續(xù)游戲時仲闽,玩家可以從之前的位置繼續(xù)進行。
備忘錄模式 本質:從發(fā)起人實體類(Originator)隔離存儲功能僵朗,降低實體類的職責赖欣。同時由于存儲信息(Memento)獨立屑彻,且存儲信息的實體交由管理類(Caretaker)管理,則可以通過為管理類擴展額外的功能對存儲信息進行擴展操作(比如增加歷史快照功能···)顶吮。
主要解決
存儲實體(Originator)狀態(tài)社牲,存儲歷史快照,回滾歷史狀態(tài)悴了。
優(yōu)缺點
優(yōu)點
- 簡化發(fā)起人實體類(Originator)職責搏恤,隔離狀態(tài)存儲與獲取,實現(xiàn)了信息的封裝湃交,客戶端無需關心狀態(tài)的保存細節(jié)熟空;
- 提供狀態(tài)回滾功能;
缺點
- 消耗資源:如果需要保存的狀態(tài)過多時搞莺,每一次保存都會消耗很多內存息罗;
使用場景
- 需要保存歷史快照的場景;
- 希望在對象之外保存狀態(tài)才沧,且除了自己其他類對象無法訪問狀態(tài)保存具體內容迈喉;
模式講解
首先來看下 備忘錄模式 的通用 UML 類圖:
從 UML 類圖中,我們可以看到温圆,備忘錄模式 主要包含三種角色:
- 發(fā)起人角色(Originator):負責創(chuàng)建一個備忘錄挨摸,記錄自身需要保存的狀態(tài);具備狀態(tài)回滾功能岁歉;
- 備忘錄角色(Memento):用于存儲 Originator 的內部狀態(tài)油坝,且可以防止 Originator 以外的對象進行訪問;
- 備忘錄管理員角色(Caretaker):負責存儲刨裆,提供,管理備忘錄(Memento)彬檀,無法對備忘錄內容進行操作和訪問帆啃;
以下是 備忘錄模式 的通用代碼:
class Client {
public static void main(String[] args) {
// 來一個發(fā)起人
Originator originator = new Originator();
// 來一個備忘錄管理員
Caretaker caretaker = new Caretaker();
// 管理員存儲發(fā)起人的備忘錄
caretaker.storeMemento(originator.createMemento());
// 發(fā)起人從管理員獲取備忘錄進行回滾
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;
}
}
// 發(fā)起人
static class Originator {
// 內部狀態(tài)
private String state;
public String getState() {
return this.state;
}
public void setState(String state) {
this.state = state;
}
// 創(chuàng)建一個備忘錄
public Memento createMemento() {
return new Memento(this.state);
}
// 從備忘錄恢復
public void restoreMemento(Memento memento) {
this.setState(memento.getState());
}
}
}
注:備忘錄模式 要求備忘錄(Memento)只對發(fā)起人(Originator)內容可見,對其他對象(Caretaker)內容不可見窍帝;但是在上面代碼中努潘,備忘錄管理員(Caretaker)其實是可以通過備忘錄(Memento)提供的相關方法(getState
,setState
)獲取和修改內部狀態(tài)坤学,這違背了 備忘錄模式 的要求疯坤,可能造成備忘錄內部狀態(tài)無意間被其他對象修改,導致發(fā)起人狀態(tài)恢復錯誤深浮,系統(tǒng)穩(wěn)定性下降压怠。
修復上述問題的代碼如下所示:
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 {
// 內部狀態(tài)
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
的私有靜態(tài)內部類,這樣就滿足了Originator
對Memento
具有寬訪問權限飞苇,但是直接這樣做菌瘫,其他類(Caretaker
)是無法訪問Memento
的蜗顽,因此在最上面我們通過定義一個空接口IMemento
,然后讓Memento
實現(xiàn)IMemento
雨让,變相將Memento
擴展為公有IMemento
雇盖,這樣就實現(xiàn)了其他類(Caretaker
)對Memento
具備窄訪問權限。這種形式才算是嚴格滿足 備忘錄模式 的設計要求栖忠。
舉個例子
例子:假設現(xiàn)有一個游戲崔挖,該游戲規(guī)定,對于之前通關的關卡庵寞,可以隨時跳回到任一關卡繼續(xù)游戲狸相。也就是說,如果你現(xiàn)在已經在第30關卡皇帮,那么30關卡之前的任一一關卷哩,你可以隨時切回去玩。請用代碼實現(xiàn)上述游戲邏輯属拾。
分析:對于已通過的關卡将谊,可以隨時切換回去玩,那么系統(tǒng)肯定是對已通過的關卡做了備份渐白,對于通關的每一關卡的相關內容都進行了存儲(為了簡單尊浓,我們只認為對關卡名字,關卡通關時間做了備份)纯衍,那么我們只需實現(xiàn)存儲功能與回滾功能即可栋齿。
具體代碼如下:
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
添加一個集合記錄游戲關卡各個記錄,使整個系統(tǒng)具備回滾功能襟诸。
運行結果如下:
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