備忘錄模式
案例
張三一天在下班回家后,想放松一下,于是打開了電腦繼續(xù)昨天沒有挑戰(zhàn)成功的 Boos 游戲關(guān)卡。這個(gè)游戲關(guān)卡相當(dāng)?shù)睦щy,張三已經(jīng)打了很多天了還是沒有挑戰(zhàn)成功,但是還好張三可以保存他在挑戰(zhàn) Boss 之前的狀態(tài),不然的話他剛剛得到的幸運(yùn)道具提升的角色狀態(tài)就會(huì)被 Boss 打掉了。下面用代碼的形式來模擬這一過程:
1.首先是玩家類:
/**
* 游戲玩家角色類
*/
public class Player {
// 玩家姓名
private String name;
// 生命值
private int vitality;
// 法力值
private int mana;
// 攻擊值
private int attack;
// 防御值
private int defense;
public Player(String name) {
this.name = name;
// 創(chuàng)建玩家時(shí)初始化各個(gè)狀態(tài)值
this.vitality = 50;
this.mana = 50;
this.attack = 50;
this.defense = 50;
show();
}
public void show() {
System.out.println("玩家{" +
"姓名='" + name + '\'' +
", 生命值=" + vitality +
", 法力值=" + mana +
", 攻擊值=" + attack +
", 防御值=" + defense +
'}');
}
// 省略 getter/setter
}
2.模擬游戲過程:
public class Main {
public static void main(String[] args) {
Player player = new Player("張三");
System.out.println("玩家[" + player.getName() + "]獲得幸運(yùn)道具獎(jiǎng)勵(lì),狀態(tài)提升↑");
player.setVitality(100);
player.setMana(100);
player.setAttack(100);
player.setDefense(100);
System.out.println("提升后各狀態(tài)值:");
player.show();
System.out.println("玩家將與 Boss PK,先保存一下獲得幸運(yùn)道具時(shí)的狀態(tài)");
Player backup = new Player(player.getName());
backup.setVitality(player.getVitality());
backup.setMana(player.getMana());
backup.setAttack(player.getAttack());
backup.setDefense(player.getDefense());
System.out.println("玩家[" + player.getName() + "]與 Boss PK,狀態(tài)下降↓");
player.setVitality(0);
player.setMana(0);
player.setAttack(10);
player.setDefense(10);
System.out.println("下降后各狀態(tài)值:");
player.show();
System.out.println("玩家恢復(fù)到獲得幸運(yùn)道具時(shí)的狀態(tài)");
player.setVitality(backup.getVitality());
player.setMana(backup.getMana());
player.setAttack(backup.getAttack());
player.setDefense(backup.getDefense());
System.out.println("恢復(fù)后各狀態(tài):");
player.show();
}
}
3.模擬結(jié)果
玩家{姓名='張三', 生命值=50, 法力值=50, 攻擊值=50, 防御值=50}
玩家[張三]獲得幸運(yùn)道具獎(jiǎng)勵(lì),狀態(tài)提升↑
提升后各狀態(tài)值:
玩家{姓名='張三', 生命值=100, 法力值=100, 攻擊值=100, 防御值=100}
玩家將與 Boss PK,先保存一下獲得幸運(yùn)道具時(shí)的狀態(tài)
玩家{姓名='張三', 生命值=50, 法力值=50, 攻擊值=50, 防御值=50}
玩家[張三]與 Boss PK,狀態(tài)下降↓
下降后各狀態(tài)值:
玩家{姓名='張三', 生命值=0, 法力值=0, 攻擊值=10, 防御值=10}
玩家恢復(fù)到獲得幸運(yùn)道具時(shí)的狀態(tài)
恢復(fù)后各狀態(tài):
玩家{姓名='張三', 生命值=100, 法力值=100, 攻擊值=100, 防御值=100}
可以看到,因?yàn)閺埲诖?Boss 之前保存了之前的狀態(tài),在挑戰(zhàn)失敗后也可以恢復(fù)到之前的狀態(tài)。這種方式與設(shè)計(jì)模式中的備忘錄模式的思想非常相似,下面就來介紹一下備忘錄模式。
模式介紹
備忘錄模式(Memento Pattern)又叫做快照模式(Snapshot Pattern)或Token模式,是GoF的23種設(shè)計(jì)模式之一,屬于行為模式。
定義:在不破壞封閉的前提下,捕獲一個(gè)對(duì)象的內(nèi)部狀態(tài),并在該對(duì)象之外保存這個(gè)狀態(tài)。這樣以后就可將該對(duì)象恢復(fù)到原先保存的狀態(tài)。
角色構(gòu)成
- Originator(原發(fā)器):它是一個(gè)普通類,可以創(chuàng)建一個(gè)備忘錄,并存儲(chǔ)它的當(dāng)前內(nèi)部狀態(tài),也可以使用備忘錄來恢復(fù)其內(nèi)部狀態(tài),一般將需要保存內(nèi)部狀態(tài)的類設(shè)計(jì)為原發(fā)器。
- Memento(備忘錄):存儲(chǔ)原發(fā)器的內(nèi)部狀態(tài),根據(jù)原發(fā)器來決定保存哪些內(nèi)部狀態(tài)。備忘錄的設(shè)計(jì)一般可以參考原發(fā)器的設(shè)計(jì),根據(jù)實(shí)際需要確定備忘錄類中的屬性。需要注意的是,除了原發(fā)器本身與負(fù)責(zé)人類之外,備忘錄對(duì)象不能直接供其他類使用,原發(fā)器的設(shè)計(jì)在不同的編程語言中實(shí)現(xiàn)機(jī)制會(huì)有所不同。
- Caretaker(負(fù)責(zé)人):負(fù)責(zé)人又稱為管理者,它負(fù)責(zé)保存?zhèn)渫?,但是不能?duì)備忘錄的內(nèi)容進(jìn)行操作或檢查。在負(fù)責(zé)人類中可以存儲(chǔ)一個(gè)或多個(gè)備忘錄對(duì)象,它只負(fù)責(zé)存儲(chǔ)對(duì)象,而不能修改對(duì)象,也無須知道對(duì)象的實(shí)現(xiàn)細(xì)節(jié)。
UML類圖

在使用備忘錄模式時(shí),首先應(yīng)該存在一個(gè)原發(fā)器類Originator,在真實(shí)業(yè)務(wù)中,原發(fā)器類是一個(gè)具體的業(yè)務(wù)類,它包含一些用于存儲(chǔ)成員數(shù)據(jù)的屬性。對(duì)于備忘錄類Memento而言,它通常提供了與原發(fā)器相對(duì)應(yīng)的屬性(可以是全部,也可以是部分)用于存儲(chǔ)原發(fā)器的狀態(tài)。對(duì)于負(fù)責(zé)人類Caretaker,它用于保存?zhèn)渫泴?duì)象,并提供getMemento()方法用于向客戶端返回一個(gè)備忘錄對(duì)象,原發(fā)器通過使用這個(gè)備忘錄對(duì)象可以回到某個(gè)歷史狀態(tài)。
代碼改造
下面就通過代碼來對(duì)上面的案例進(jìn)行改造:
1.原發(fā)器類,很明顯就是這里的玩家角色類:
/**
* 原發(fā)器類
*/
public class Player {
// 玩家姓名
private String name;
// 生命值
private int vitality;
// 法力值
private int mana;
// 攻擊值
private int attack;
// 防御值
private int defense;
public Player(String name) {
this.name = name;
// 創(chuàng)建玩家時(shí)初始化各個(gè)狀態(tài)默認(rèn)值
this.vitality = 50;
this.mana = 50;
this.attack = 50;
this.defense = 50;
show();
}
// 創(chuàng)建一個(gè)備忘錄對(duì)象
public Memento createMemento() {
return new Memento(this);
}
// 根據(jù)備忘錄對(duì)象恢復(fù)原發(fā)器狀態(tài)
public void restoreMemento(Memento memento) {
this.name = memento.getName();
this.vitality = memento.getVitality();
this.mana = memento.getMana();
this.attack = memento.getAttack();
this.defense = memento.getDefense();
}
public void show() {
System.out.println("玩家{" +
"姓名='" + name + '\'' +
", 生命值=" + vitality +
", 法力值=" + mana +
", 攻擊值=" + attack +
", 防御值=" + defense +
'}');
}
// 省略 getter/setter...
}
2.備忘錄類:
/**
* 備忘錄角色類
*/
public class Memento {
// 玩家姓名
private String name;
// 生命值
private int vitality;
// 法力值
private int mana;
// 攻擊值
private int attack;
// 防御值
private int defense;
public Memento(Player player) {
this.name = player.getName();
this.vitality = player.getVitality();
this.mana = player.getMana();
this.attack = player.getAttack();
this.defense = player.getDefense();
}
// 省略 getter/setter...
}
3.負(fù)責(zé)人類:
/**
* 負(fù)責(zé)人類
*/
public class Caretaker {
private Memento memento;
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}
4.模擬過程:
public class Main {
public static void main(String[] args) {
Caretaker caretaker = new Caretaker();
Player player = new Player("張三");
System.out.println("玩家[" + player.getName() + "]獲得幸運(yùn)道具獎(jiǎng)勵(lì),狀態(tài)提升↑");
player.setVitality(100);
player.setMana(100);
player.setAttack(100);
player.setDefense(100);
System.out.println("提升后各狀態(tài)值:");
player.show();
System.out.println("玩家將與 Boss PK,先保存一下獲得幸運(yùn)道具時(shí)的狀態(tài)");
caretaker.setMemento(player.createMemento());
System.out.println("玩家[" + player.getName() + "]與 Boss PK,狀態(tài)下降↓");
player.setVitality(0);
player.setMana(0);
player.setAttack(10);
player.setDefense(10);
System.out.println("下降后各狀態(tài)值:");
player.show();
System.out.println("玩家恢復(fù)到獲得幸運(yùn)道具時(shí)的狀態(tài)");
player.restoreMemento(caretaker.getMemento());
System.out.println("恢復(fù)后各狀態(tài):");
player.show();
}
}
5.模擬結(jié)果:
玩家{姓名='張三', 生命值=50, 法力值=50, 攻擊值=50, 防御值=50}
玩家[張三]獲得幸運(yùn)道具獎(jiǎng)勵(lì),狀態(tài)提升↑
提升后各狀態(tài)值:
玩家{姓名='張三', 生命值=100, 法力值=100, 攻擊值=100, 防御值=100}
玩家將與 Boss PK,先保存一下獲得幸運(yùn)道具時(shí)的狀態(tài)
玩家[張三]與 Boss PK,狀態(tài)下降↓
下降后各狀態(tài)值:
玩家{姓名='張三', 生命值=0, 法力值=0, 攻擊值=10, 防御值=10}
玩家恢復(fù)到獲得幸運(yùn)道具時(shí)的狀態(tài)
恢復(fù)后各狀態(tài):
玩家{姓名='張三', 生命值=100, 法力值=100, 攻擊值=100, 防御值=100}
可以看到效果是一樣的,但是使用了備忘錄模式之后,他將恢復(fù)前的狀態(tài)用備忘錄類Memento類來進(jìn)行保存,在恢復(fù)時(shí)用負(fù)責(zé)人類Caretaker類進(jìn)行保存?zhèn)渫?,使得類之間的職責(zé)更加清晰。
模式應(yīng)用
備忘錄模式在很多軟件的使用過程中普遍存在,但是在應(yīng)用軟件開發(fā)中,它的使用頻率并不太高,因?yàn)楝F(xiàn)在很多基于窗體和瀏覽器的應(yīng)用軟件并沒有提供撤銷操作。如果需要為軟件提供撤銷功能,備忘錄模式無疑是一種很好的解決方案。在一些字處理軟件、圖像編輯軟件、數(shù)據(jù)庫管理系統(tǒng)等軟件中備忘錄模式都得到了很好的應(yīng)用。下面列舉一些備忘錄思想的場(chǎng)景:瀏覽器回退、數(shù)據(jù)庫備份與還原、編輯器撤銷與重做、棋牌游戲悔棋等。
總結(jié)
1.主要優(yōu)點(diǎn)
- 它提供了一種狀態(tài)恢復(fù)的實(shí)現(xiàn)機(jī)制,使得用戶可以方便地回到一個(gè)特定的歷史步驟,當(dāng)新的狀態(tài)無效或者存在問題時(shí),可以使用暫時(shí)存儲(chǔ)起來的備忘錄將狀態(tài)復(fù)原。
- 備忘錄實(shí)現(xiàn)了對(duì)信息的封裝,一個(gè)備忘錄對(duì)象是一種原發(fā)器對(duì)象狀態(tài)的表示,不會(huì)被其他代碼所改動(dòng)。備忘錄保存了原發(fā)器的狀態(tài),采用列表、堆棧等集合來存儲(chǔ)備忘錄對(duì)象可以實(shí)現(xiàn)多次撤銷操作。
2.主要缺點(diǎn)
- 資源消耗過大,如果需要保存的原發(fā)器類的成員變量太多,就不可避免需要占用大量的存儲(chǔ)空間,每保存一次對(duì)象的狀態(tài)都需要消耗一定的系統(tǒng)資源。
3.適用場(chǎng)景
- 保存一個(gè)對(duì)象在某一個(gè)時(shí)刻的全部狀態(tài)或部分狀態(tài),這樣以后需要時(shí)它能夠恢復(fù)到先前的狀態(tài),實(shí)現(xiàn)撤銷操作。
- 防止外界對(duì)象破壞一個(gè)對(duì)象歷史狀態(tài)的封裝性,避免將對(duì)象歷史狀態(tài)的實(shí)現(xiàn)細(xì)節(jié)暴露給外界對(duì)象。
參考資料
- 大話設(shè)計(jì)模式
- 設(shè)計(jì)模式Java版本-劉偉
- 設(shè)計(jì)模式 | 備忘錄模式及典型應(yīng)用
本篇文章github代碼地址:https://github.com/Phoegel/design-pattern/tree/main/memento
轉(zhuǎn)載請(qǐng)說明出處,本篇博客地址:http://www.itdecent.cn/p/fda7a3c871b1