個人比較喜歡玩單機游戲,什么仙劍、古劍、鬼泣、使命召喚、三國無雙等等一系列的游戲我都玩過(現在期待凡人修仙傳),對於這些游戲除了劇情好、場面大、爽快之外,還可以隨時存檔,等到下次想玩了又可以從剛開始的位置玩起(貌似現在的游戲都有)。這里的所謂存檔就是將當前的狀態記錄下來,打開存檔的時候只需要將這些狀態讀取出來即可。還有我們小時候期待長大,長大了希望回到小時候。18歲想長到28歲,到了28想還是18好!!!
上面所提到的就是“后悔葯”機制。在我們生活中,做錯事了我們都期望可以從新開始,希望這個世界上有后悔葯可以吃,但現實是殘酷的,這個世界是沒有后悔葯可以吃!雖然在現實社會中不可以實現,我們可以在軟件的世界里實現(現在好多軟件有撤銷功能:Ctrl+Z不就是么)。
在應用開發中,很多時候我們總是需要記錄一個對象的內部狀態,這樣做的目的就是為了允許用戶取消不確定或者錯誤的操作,能夠恢復到他原先的狀態,使得他有“后悔葯”可吃。
備忘錄模式是一種給我們的軟件提供后悔葯的機制,通過它可以使系統恢復到某一特定的歷史狀態。
一、 模式定義
所謂備忘錄模式就是在不破壞封裝的前提下,捕獲一個對象的內部狀態,並在該對象之外保存這個狀態,這樣可以在以后將對象恢復到原先保存的狀態。
備忘錄模式將要保存的細節給封裝在備忘錄中,就是那天要改變保存的細節也不會影響到客戶端。
二、 模式結構
下圖是備忘錄模式的UML結構圖:
備忘錄模式主要包含入下幾個角色:
Originator: 原發器。負責創建一個備忘錄,用以記錄當前對象的內部狀態,通過也可以使用它來利用備忘錄恢復內部狀態。同時原發器還可以根據需要決定Memento存儲Originator的那些內部狀態。
Memento: 備忘錄。用於存儲Originator的內部狀態,並且可以防止Originator以外的對象訪問Memento。在備忘錄Memento中有兩個接口,其中Caretaker只能看到備忘錄中的窄接口,它只能將備忘錄傳遞給其他對象。Originator可以看到寬接口,允許它訪問返回到先前狀態的所有數據。
Caretaker: 負責人。負責保存好備忘錄,不能對備忘錄的內容進行操作和訪問,只能夠將備忘錄傳遞給其他對象。
在備忘錄模式中,最重要的就是備忘錄Memento了。我們都是備忘錄中存儲的就是原發器的部分或者所有的狀態信息,而這些狀態信息是不能夠被其他對象所訪問了,也就是說我們是不可能在備忘錄之外的對象來存儲這些狀態信息,如果暴漏了內部狀態信息就違反了封裝的原則,故備忘錄是除了原發器外其他對象都是不可以訪問的。
所以為了實現備忘錄模式的封裝,我們需要對備忘錄的訪問做些控制:
對原發器:可以訪問備忘錄里的所有信息。
對負責人:不可以訪問備忘錄里面的數據,但是他可以保存備忘錄並且可以將備忘錄傳遞給其他對象。
其他對象:不可訪問也不可以保存,它只負責接收從負責人那里傳遞過來的備忘錄同時恢復原發器的狀態。
所以就備忘錄模式而言理想的情況就是只允許生成該備忘錄的那個原發器訪問備忘錄的內部狀態。
典型的備忘錄代碼如下:
class Memento { private String state; public Memento(Originator o){ state = o.state; } public void setState(String state){ this.state=state; } public String getState(){ return this.state; } }
三、 模式實現
實現場景:我們就以游戲挑戰BOSS為實現場景,在挑戰BOSS之前,角色的血量、藍量都是滿值,然后存檔,在大戰BOSS時,由於操作失誤導致血量和藍量大量損耗,所以只好恢復到剛剛開始的存檔點,繼續進行大戰BOSS了。這里使用備忘錄模式來實現。UML結構圖如下:
首先是游戲角色類:Role.java
private int bloodFlow; private int magicPoint; public Role(int bloodFlow,int magicPoint){ this.bloodFlow = bloodFlow; this.magicPoint = magicPoint; } public int getBloodFlow() { return bloodFlow; } public void setBloodFlow(int bloodFlow) { this.bloodFlow = bloodFlow; } public int getMagicPoint() { return magicPoint; } public void setMagicPoint(int magicPoint) { this.magicPoint = magicPoint; } /** * @desc 展示角色當前狀態 * @return void */ public void display(){ System.out.println("用戶當前狀態:"); System.out.println("血量:" + getBloodFlow() + ";藍量:" + getMagicPoint()); } /** * @desc 保持存檔、當前狀態 * @return * @return Memento */ public Memento saveMemento(){ return new Memento(getBloodFlow(), getMagicPoint()); } /** * @desc 恢復存檔 * @param memento * @return void */ public void restoreMemento(Memento memento){ this.bloodFlow = memento.getBloodFlow(); this.magicPoint = memento.getMagicPoint(); } }
備忘錄:Memento.java
class Memento { private int bloodFlow; private int magicPoint; public int getBloodFlow() { return bloodFlow; } public void setBloodFlow(int bloodFlow) { this.bloodFlow = bloodFlow; } public int getMagicPoint() { return magicPoint; } public void setMagicPoint(int magicPoint) { this.magicPoint = magicPoint; } public Memento(int bloodFlow,int magicPoint){ this.bloodFlow = bloodFlow; this.magicPoint = magicPoint; } }
負責人:Caretaker.java
public class Caretaker { Memento memento; public Memento getMemento() { return memento; } public void setMemento(Memento memento) { this.memento = memento; } }
public class Client { public static void main(String[] args) { //打BOSS之前:血、藍全部滿值 Role role = new Role(100, 100); System.out.println("----------大戰BOSS之前----------"); role.display(); //保持進度 Caretaker caretaker = new Caretaker(); caretaker.memento = role.saveMemento(); //大戰BOSS,快come Over了 role.setBloodFlow(20); role.setMagicPoint(20); System.out.println("----------大戰BOSS----------"); role.display(); //恢復存檔 role.restoreMemento(caretaker.getMemento()); System.out.println("----------恢復----------"); role.display(); } }
運行結果
四、 模式的優缺點
優點
1、 給用戶提供了一種可以恢復狀態的機制。可以是用戶能夠比較方便地回到某個歷史的狀態。
2、 實現了信息的封裝。使得用戶不需要關心狀態的保存細節。
缺點
消耗資源。如果類的成員變量過多,勢必會占用比較大的資源,而且每一次保存都會消耗一定的內存。
五、 模式適用場景
1、 需要保存一個對象在某一個時刻的狀態或部分狀態。
2、 如果用一個接口來讓其他對象得到這些狀態,將會暴露對象的實現細節並破壞對象的封裝性,一個對象不希望外界直接訪問其內部狀態,通過負責人可以間接訪問其內部狀態。
六、 模式總結
1、 備忘錄模式可以實現在不破壞封裝的前提下,捕獲一個類的內部狀態,並且在該對象之外保存該對象的狀態,保證該對象能夠恢復到歷史的某個狀態。
2、 備忘錄模式實現了內部狀態的封裝,除了創建它的原發器之外其他對象都不能夠訪問它。
3、 備忘錄模式會占用較多的內存,消耗資源。