1 場景問題#
1.1 開發(fā)仿真系統(tǒng)##
考慮這樣一個(gè)仿真應(yīng)用,功能是:模擬運(yùn)行針對某個(gè)具體問題的多個(gè)解決方案,記錄運(yùn)行過程的各種數(shù)據(jù),在模擬運(yùn)行完成過后,好對這多個(gè)解決方案進(jìn)行比較和評價(jià),從而選定最優(yōu)的解決方案。
這種仿真系統(tǒng),在很多領(lǐng)域都有應(yīng)用,比如:工作流系統(tǒng),對同一問題制定多個(gè)流程,然后通過仿真運(yùn)行,最后來確定最優(yōu)的流程做為解決方案;在工業(yè)設(shè)計(jì)和制造領(lǐng)域,仿真系統(tǒng)的應(yīng)用就更廣泛了。
由于都是解決同一個(gè)具體的問題,這多個(gè)解決方案并不是完全不一樣的,假定它們的前半部分運(yùn)行是完全一樣的,只是在后半部分采用了不同的解決方案,后半部分需要使用前半部分運(yùn)行所產(chǎn)生的數(shù)據(jù)。
由于要模擬運(yùn)行多個(gè)解決方案,而且最后要根據(jù)運(yùn)行結(jié)果來進(jìn)行評價(jià),這就意味著每個(gè)方案的后半部分的初始數(shù)據(jù)應(yīng)該是一樣,也就是說在運(yùn)行每個(gè)方案后半部分之前,要保證數(shù)據(jù)都是由前半部分運(yùn)行所產(chǎn)生的數(shù)據(jù)
,當(dāng)然,咱們這里并不具體的去深入到底有哪些解決方案,也不去深入到底有哪些狀態(tài)數(shù)據(jù),這里只是示意一下。
那么,這樣的系統(tǒng)該如何實(shí)現(xiàn)呢?尤其是每個(gè)方案運(yùn)行需要的初始數(shù)據(jù)應(yīng)該一樣,要如何來保證呢?
1.2 不用模式的解決方案##
要保證初始數(shù)據(jù)的一致,實(shí)現(xiàn)思路也很簡單:
首先模擬運(yùn)行流程第一個(gè)階段,得到后階段各個(gè)方案運(yùn)行需要的數(shù)據(jù),并把數(shù)據(jù)保存下來,以備后用;
每次在模擬運(yùn)行某一個(gè)方案之前,用保存的數(shù)據(jù)去重新設(shè)置模擬運(yùn)行流程的對象,這樣運(yùn)行后面不同的方案時(shí),對于這些方案,初始數(shù)據(jù)就是一樣的了;
根據(jù)上面的思路,來寫出仿真運(yùn)行的示意代碼,示例代碼如下:
/**
* 模擬運(yùn)行流程A,只是一個(gè)示意,代指某個(gè)具體流程
*/
public class FlowAMock {
/**
* 流程名稱,不需要外部存儲(chǔ)的狀態(tài)數(shù)據(jù)
*/
private String flowName;
/**
* 示意,代指某個(gè)中間結(jié)果,需要外部存儲(chǔ)的狀態(tài)數(shù)據(jù)
*/
private int tempResult;
/**
* 示意,代指某個(gè)中間結(jié)果,需要外部存儲(chǔ)的狀態(tài)數(shù)據(jù)
*/
private String tempState;
/**
* 構(gòu)造方法,傳入流程名稱
* @param flowName 流程名稱
*/
public FlowAMock(String flowName){
this.flowName = flowName;
}
public String getTempState() {
return tempState;
}
public void setTempState(String tempState) {
this.tempState = tempState;
}
public int getTempResult() {
return tempResult;
}
public void setTempResult(int tempResult) {
this.tempResult = tempResult;
}
/**
* 示意,運(yùn)行流程的第一個(gè)階段
*/
public void runPhaseOne(){
//在這個(gè)階段,可能產(chǎn)生了中間結(jié)果,示意一下
tempResult = 3;
tempState = "PhaseOne";
}
/**
* 示意,按照方案一來運(yùn)行流程后半部分
*/
public void schema1(){
//示意,需要使用第一個(gè)階段產(chǎn)生的數(shù)據(jù)
this.tempState += ",Schema1";
System.out.println(this.tempState + " : now run "+tempResult);
this.tempResult += 11;
}
/**
* 示意,按照方案二來運(yùn)行流程后半部分
*/
public void schema2(){
//示意,需要使用第一個(gè)階段產(chǎn)生的數(shù)據(jù)
this.tempState += ",Schema2";
System.out.println(this.tempState + " : now run "+tempResult);
this.tempResult += 22;
}
}
看看如何使用這個(gè)模擬流程的對象,寫個(gè)客戶端來測試一下。示例代碼如下:
public class Client {
public static void main(String[] args) {
// 創(chuàng)建模擬運(yùn)行流程的對象
FlowAMock mock = new FlowAMock("TestFlow");
//運(yùn)行流程的第一個(gè)階段
mock.runPhaseOne();
//得到第一個(gè)階段運(yùn)行所產(chǎn)生的數(shù)據(jù),后面要用
int tempResult = mock.getTempResult();
String tempState = mock.getTempState();
//按照方案一來運(yùn)行流程后半部分
mock.schema1();
//把第一個(gè)階段運(yùn)行所產(chǎn)生的數(shù)據(jù)重新設(shè)置回去
mock.setTempResult(tempResult);
mock.setTempState(tempState);
//按照方案二來運(yùn)行流程后半部分
mock.schema2();
}
}
運(yùn)行結(jié)果如下:
PhaseOne,Schema1 : now run 3
PhaseOne,Schema2 : now run 3
仔細(xì)看,上面結(jié)果中框住的部分,是一樣的值,這說明運(yùn)行時(shí),它們的初始數(shù)據(jù)是一樣的,基本滿足了功能要求。
1.3 有何問題##
看起來實(shí)現(xiàn)很簡單,是吧,想一想有沒有什么問題呢?
上面的實(shí)現(xiàn)有一個(gè)不太好的地方,那就是數(shù)據(jù)是一個(gè)一個(gè)零散著在外部存放的,如果需要外部存放的數(shù)據(jù)多了,會(huì)顯得很雜亂
。這個(gè)好解決,只需要定義一個(gè)數(shù)據(jù)對象來封裝這些需要外部存放的數(shù)據(jù)就可以了,上面那樣做是故意的,好提醒大家這個(gè)問題。這個(gè)就不去示例了。
還有一個(gè)嚴(yán)重的問題,那就是:為了把運(yùn)行期間的數(shù)據(jù)放到外部存儲(chǔ)起來,模擬流程的對象被迫把內(nèi)部數(shù)據(jù)結(jié)構(gòu)開放出來,這暴露了對象的實(shí)現(xiàn)細(xì)節(jié),而且也破壞了對象的封裝性
。本來這些數(shù)據(jù)只是模擬流程的對象內(nèi)部數(shù)據(jù),應(yīng)該是不對外的。
那么究竟如何實(shí)現(xiàn)這樣的功能會(huì)比較好呢?
2 解決方案#
2.1 備忘錄模式來解決##
來解決上述問題的一個(gè)合理的解決方案就是備忘錄模式。那么什么是備忘錄模式呢?
- 備忘錄模式定義
一個(gè)備忘錄是一個(gè)對象,它存儲(chǔ)另一個(gè)對象在某個(gè)瞬間的內(nèi)部狀態(tài),后者被稱為備忘錄的原發(fā)器。
- 應(yīng)用備忘錄模式來解決的思路
仔細(xì)分析上面的示例功能,需要在運(yùn)行期間捕獲模擬流程運(yùn)行的對象的內(nèi)部狀態(tài),這些需要捕獲的內(nèi)部狀態(tài)就是它運(yùn)行第一個(gè)階段產(chǎn)生的內(nèi)部數(shù)據(jù),并且在該對象之外來保存這些狀態(tài),因?yàn)樵诤竺嫠胁煌倪\(yùn)行方案
。但是這些不同的運(yùn)行方案需要的初始數(shù)據(jù)是一樣的,都是流程在第一個(gè)階段運(yùn)行所產(chǎn)生的數(shù)據(jù),這就要求運(yùn)行每個(gè)方案后半部分前,要把該對象的狀態(tài)恢復(fù)回到第一個(gè)階段運(yùn)行結(jié)束時(shí)候的狀態(tài)
。
在這個(gè)示例中出現(xiàn)的、需要解決的問題就是:如何能夠在不破壞對象的封裝性的前提下,來保存和恢復(fù)對象的狀態(tài)
。
看起來跟備忘錄模式要解決的問題是如此的貼切,簡直備忘錄模式像是專為這個(gè)應(yīng)用打造的一樣。那么使用備忘錄模式如何來解決這個(gè)問題呢?
備忘錄模式引入一個(gè)存儲(chǔ)狀態(tài)的備忘錄對象,為了讓外部無法訪問這個(gè)對象的值,一般把這個(gè)對象實(shí)現(xiàn)成為需要保存數(shù)據(jù)的對象的內(nèi)部類,通常還是私有的,這樣一來,除了這個(gè)需要保存數(shù)據(jù)的對象,外部無法訪問到這個(gè)備忘錄對象的數(shù)據(jù),這就保證了對象的封裝性不被破壞
。
但是這個(gè)備忘錄對象需要存儲(chǔ)在外部,為了避免讓外部訪問到這個(gè)對象內(nèi)部的數(shù)據(jù),備忘錄模式引入了一個(gè)備忘錄對象的窄接口,這個(gè)接口一般是空的,什么方法都沒有,這樣外部存儲(chǔ)的地方,只是知道存儲(chǔ)了一些備忘錄接口的對象,但是由于接口是空的,它們無法通過接口去訪問備忘錄對象內(nèi)的數(shù)據(jù)
。
2.2 模式結(jié)構(gòu)和說明##
備忘錄模式結(jié)構(gòu)如圖所示:
Memento:
備忘錄。主要用來存儲(chǔ)原發(fā)器對象的內(nèi)部狀態(tài),但是具體需要存儲(chǔ)哪些數(shù)據(jù)是由原發(fā)器對象來決定的
。另外備忘錄應(yīng)該只能由原發(fā)器對象來訪問它內(nèi)部的數(shù)據(jù),原發(fā)器外部的對象不應(yīng)該能訪問到備忘錄對象的內(nèi)部數(shù)據(jù)。Originator:原發(fā)器。
使用備忘錄來保存某個(gè)時(shí)刻原發(fā)器自身的狀態(tài),也可以使用備忘錄來恢復(fù)內(nèi)部狀態(tài)
。Caretaker:備忘錄管理者,或者稱為備忘錄負(fù)責(zé)人。
主要負(fù)責(zé)保存?zhèn)渫泴ο?,但是不能對備忘錄對象的?nèi)容進(jìn)行操作或檢查
。
2.3 備忘錄模式示例代碼##
- 先看看備忘錄對象的窄接口,就是那個(gè)Memento接口,這個(gè)實(shí)現(xiàn)最簡單,是個(gè)空的接口,沒有任何方法定義,示例代碼如下:
/**
* 備忘錄的窄接口,沒有任何方法定義
*/
public interface Memento {
//
}
- 看看原發(fā)器對象,它里面會(huì)有備忘錄對象的實(shí)現(xiàn),因?yàn)檎嬲膫渫泴ο螽?dāng)作原發(fā)器對象的一個(gè)私有內(nèi)部類來實(shí)現(xiàn)了。示例代碼如下:
/**
* 原發(fā)器對象
*/
public class Originator {
/**
* 示意,表示原發(fā)器的狀態(tài)
*/
private String state = "";
/**
* 創(chuàng)建保存原發(fā)器對象的狀態(tài)的備忘錄對象
* @return 創(chuàng)建好的備忘錄對象
*/
public Memento createMemento() {
return new MementoImpl(state);
}
/**
* 重新設(shè)置原發(fā)器對象的狀態(tài),讓其回到備忘錄對象記錄的狀態(tài)
* @param memento 記錄有原發(fā)器狀態(tài)的備忘錄對象
*/
public void setMemento(Memento memento) {
MementoImpl mementoImpl = (MementoImpl)memento;
this.state = mementoImpl.getState();
}
/**
* 真正的備忘錄對象,實(shí)現(xiàn)備忘錄窄接口
* 實(shí)現(xiàn)成私有的內(nèi)部類,不讓外部訪問
*/
private static class MementoImpl implements Memento{
/**
* 示意,表示需要保存的狀態(tài)
*/
private String state = "";
public MementoImpl(String state){
this.state = state;
}
public String getState() {
return state;
}
}
}
- 接下來看看備忘錄管理者對象,示例代碼如下:
/**
* 負(fù)責(zé)保存?zhèn)渫浀膶ο? */
public class Caretaker{
/**
* 記錄被保存的備忘錄對象
*/
private Memento memento = null;
/**
* 保存?zhèn)渫泴ο? * @param memento 被保存的備忘錄對象
*/
public void saveMemento(Memento memento){
this.memento = memento;
}
/**
* 獲取被保存的備忘錄對象
* @return 被保存的備忘錄對象
*/
public Memento retriveMemento(){
return this.memento;
}
}
2.4 使用備忘錄模式重寫示例##
學(xué)習(xí)了備忘錄模式的基本知識(shí)過后,來嘗試一下,使用備忘錄模式把前面的示例重寫一下,好看看如何使用備忘錄模式。
首先,那個(gè)模擬流程運(yùn)行的對象,就相當(dāng)于備忘錄模式中的原發(fā)器;
而它要保存的數(shù)據(jù),原來是零散的,現(xiàn)在做一個(gè)備忘錄對象來存儲(chǔ)這些數(shù)據(jù),并且把這個(gè)備忘錄對象實(shí)現(xiàn)成為內(nèi)部類;
當(dāng)然為了保存這個(gè)備忘錄對象,還是需要提供管理者對象的;
為了和管理者對象交互,管理者需要知道保存對象的類型,那就提供一個(gè)備忘錄對象的窄接口來供管理者使用,相當(dāng)于標(biāo)識(shí)了類型
。
此時(shí)程序的結(jié)構(gòu)如圖所示:
- 先來看看備忘錄對象的窄接口吧,示例代碼如下:
/**
* 模擬運(yùn)行流程A的對象的備忘錄接口,是個(gè)窄接口
*/
public interface FlowAMockMemento {
//空的
}
- 再來看看新的模擬運(yùn)行流程A的對象,相當(dāng)于原發(fā)器對象了,它的變化比較多,大致有如下變化:
首先這個(gè)對象原來暴露出去的內(nèi)部狀態(tài),不用再暴露出去了,也就是內(nèi)部狀態(tài)不用再對外提供getter/setter方法了;
在這個(gè)對象里面提供一個(gè)私有的備忘錄對象,里面封裝想要保存的內(nèi)部狀態(tài),同時(shí)讓這個(gè)備忘錄對象實(shí)現(xiàn)備忘錄對象的窄接口;
在這個(gè)對象里面提供創(chuàng)建備忘錄對象,和根據(jù)備忘錄對象恢復(fù)內(nèi)部狀態(tài)的方法;
具體的示例代碼如下:
/**
* 模擬運(yùn)行流程A,只是一個(gè)示意,代指某個(gè)具體流程
*/
public class FlowAMock {
/**
* 流程名稱,不需要外部存儲(chǔ)的狀態(tài)數(shù)據(jù)
*/
private String flowName;
/**
* 示意,代指某個(gè)中間結(jié)果,需要外部存儲(chǔ)的狀態(tài)數(shù)據(jù)
*/
private int tempResult;
/**
* 示意,代指某個(gè)中間結(jié)果,需要外部存儲(chǔ)的狀態(tài)數(shù)據(jù)
*/
private String tempState;
/**
* 構(gòu)造方法,傳入流程名稱
* @param flowName 流程名稱
*/
public FlowAMock(String flowName){
this.flowName = flowName;
}
/**
* 示意,運(yùn)行流程的第一個(gè)階段
*/
public void runPhaseOne(){
//在這個(gè)階段,可能產(chǎn)生了中間結(jié)果,示意一下
tempResult = 3;
tempState = "PhaseOne";
}
/**
* 示意,按照方案一來運(yùn)行流程后半部分
*/
public void schema1(){
//示意,需要使用第一個(gè)階段產(chǎn)生的數(shù)據(jù)
this.tempState += ",Schema1";
System.out.println(this.tempState + " : now run "+tempResult);
this.tempResult += 11;
}
/**
* 示意,按照方案二來運(yùn)行流程后半部分
*/
public void schema2(){
//示意,需要使用第一個(gè)階段產(chǎn)生的數(shù)據(jù)
this.tempState += ",Schema2";
System.out.println(this.tempState + " : now run "+tempResult);
this.tempResult += 22;
}
/**
* 創(chuàng)建保存原發(fā)器對象的狀態(tài)的備忘錄對象
* @return 創(chuàng)建好的備忘錄對象
*/
public FlowAMockMemento createMemento() {
return new MementoImpl(this.tempResult,this.tempState);
}
/**
* 重新設(shè)置原發(fā)器對象的狀態(tài),讓其回到備忘錄對象記錄的狀態(tài)
* @param memento 記錄有原發(fā)器狀態(tài)的備忘錄對象
*/
public void setMemento(FlowAMockMemento memento) {
MementoImpl mementoImpl = (MementoImpl)memento;
this.tempResult = mementoImpl.getTempResult();
this.tempState = mementoImpl.getTempState();
}
/**
* 真正的備忘錄對象,實(shí)現(xiàn)備忘錄窄接口
* 實(shí)現(xiàn)成私有的內(nèi)部類,不讓外部訪問
*/
private static class MementoImpl implements FlowAMockMemento{
/**
* 示意,保存某個(gè)中間結(jié)果
*/
private int tempResult;
/**
* 示意,保存某個(gè)中間結(jié)果
*/
private String tempState;
public MementoImpl(int tempResult,String tempState){
this.tempResult = tempResult;
this.tempState = tempState;
}
public int getTempResult() {
return tempResult;
}
public String getTempState() {
return tempState;
}
}
}
- 接下來要來實(shí)現(xiàn)提供保存?zhèn)渫泴ο蟮墓芾碚吡?,示例代碼如下:
/**
* 負(fù)責(zé)保存模擬運(yùn)行流程A的對象的備忘錄對象
*/
public class FlowAMementoCareTaker {
/**
* 記錄被保存的備忘錄對象
*/
private FlowAMockMemento memento = null;
/**
* 保存?zhèn)渫泴ο? * @param memento 被保存的備忘錄對象
*/
public void saveMemento(FlowAMockMemento memento){
this.memento = memento;
}
/**
* 獲取被保存的備忘錄對象
* @return 被保存的備忘錄對象
*/
public FlowAMockMemento retriveMemento(){
return this.memento;
}
}
- 最后來看看,如何使用上面按照備忘錄模式實(shí)現(xiàn)的這些對象呢,寫個(gè)新的客戶端來測試一下,示例代碼如下:
public class Client {
public static void main(String[] args) {
// 創(chuàng)建模擬運(yùn)行流程的對象
FlowAMock mock = new FlowAMock("TestFlow");
//運(yùn)行流程的第一個(gè)階段
mock.runPhaseOne();
//創(chuàng)建一個(gè)管理者
FlowAMementoCareTaker careTaker = new FlowAMementoCareTaker();
//創(chuàng)建此時(shí)對象的備忘錄對象,并保存到管理者對象那里,后面要用
FlowAMockMemento memento = mock.createMemento();
careTaker.saveMemento(memento);
//按照方案一來運(yùn)行流程后半部分
mock.schema1();
//從管理者獲取備忘錄對象,然后設(shè)置回去,
//讓模擬運(yùn)行流程的對象自己恢復(fù)自己的內(nèi)部狀態(tài)
mock.setMemento(careTaker.retriveMemento());
//按照方案二來運(yùn)行流程后半部分
mock.schema2();
}
}
運(yùn)行結(jié)果跟前面的示例是一樣的,結(jié)果如下:
PhaseOne,Schema1 : now run 3
PhaseOne,Schema2 : now run 3
好好體會(huì)一下上面的示例,由于備忘錄對象是一個(gè)私有的內(nèi)部類,外面只能通過備忘錄對象的窄接口來獲取備忘錄對象,而這個(gè)接口沒有任何方法,僅僅起到了一個(gè)標(biāo)識(shí)對象類型的作用,從而保證內(nèi)部的數(shù)據(jù)不會(huì)被外部獲取或是操作,保證了原發(fā)器對象的封裝性,也就不再暴露原發(fā)器對象的內(nèi)部結(jié)構(gòu)了
。
3 模式講解#
3.1 認(rèn)識(shí)備忘錄模式##
- 備忘錄模式的功能
備忘錄模式的功能,首先是在不破壞封裝性的前提下,捕獲一個(gè)對象的內(nèi)部狀
態(tài)。這里要注意兩點(diǎn),一個(gè)是不破壞封裝性,也就是對象不能暴露它不應(yīng)該暴露的細(xì)節(jié)
;另外一個(gè)是捕獲的是對象的內(nèi)部狀態(tài),而且通常還是運(yùn)行期間某個(gè)時(shí)刻,對象的內(nèi)部狀態(tài)
。
為什么要捕獲這個(gè)對象的內(nèi)部狀態(tài)呢?捕獲這個(gè)內(nèi)部狀態(tài)有什么用呢?
是要在以后的某個(gè)時(shí)候,將該對象的狀態(tài)恢復(fù)到備忘錄所保存的狀態(tài),這才是備忘錄真正的目的
,前面保存狀態(tài)就是為了后面恢復(fù),雖然不是一定要恢復(fù),但是目的是為了恢復(fù)。這也是很多人理解備忘錄模式的時(shí)候,忽視掉的地方,他們太關(guān)注備忘,而忽視了恢復(fù),這是不全面的理解。
捕獲的狀態(tài)存放在哪里呢?
備忘錄模式中,捕獲的內(nèi)部狀態(tài),存儲(chǔ)在備忘錄對象中
;而備忘錄對象,通常會(huì)被存儲(chǔ)在原發(fā)器對象之外
,也就是被保存狀態(tài)的對象的外部,通常是存放在管理者對象哪里。
- 備忘錄對象
在備忘錄模式中,備忘錄對象,通常就是用來記錄原發(fā)器需要保存的狀態(tài)的對象
,簡單點(diǎn)的實(shí)現(xiàn),也就是個(gè)封裝數(shù)據(jù)的對象。
但是這個(gè)備忘錄對象和普通的封裝數(shù)據(jù)的對象還是有區(qū)別的,主要就是這個(gè)備忘錄對象,一般只讓原發(fā)器對象來操作,而不是像普通的封裝數(shù)據(jù)的對象那樣,誰都可以使用
。為了保證這一點(diǎn),通常會(huì)把備忘錄對象作為原發(fā)器對象的內(nèi)部類來實(shí)現(xiàn),而且會(huì)實(shí)現(xiàn)成私有的,這就斷絕了外部來訪問這個(gè)備忘錄對象的途徑
。
但是備忘錄對象需要保存在原發(fā)器對象之外,為了與外部交互,通常備忘錄對象都會(huì)實(shí)現(xiàn)一個(gè)窄接口,來標(biāo)識(shí)對象的類型
。
- 原發(fā)器對象
原發(fā)器對象,就是需要被保存狀態(tài)的對象,也是有可能需要恢復(fù)狀態(tài)的對象
。原發(fā)器一般會(huì)包含備忘錄對象的實(shí)現(xiàn)
。
通常原發(fā)器對象應(yīng)該提供捕獲某個(gè)時(shí)刻對象內(nèi)部狀態(tài)的方法
,在這個(gè)方法里面,原發(fā)器對象會(huì)創(chuàng)建備忘錄對象,把需要保存的狀態(tài)數(shù)據(jù)設(shè)置到備忘錄對象中,然后把備忘錄對象提供給管理者對象來保存。
當(dāng)然,原發(fā)器對象也應(yīng)該提供這樣的方法:按照外部要求來恢復(fù)內(nèi)部狀態(tài)到某個(gè)備忘錄對象記錄的狀態(tài)
。
- 管理者對象
在備忘錄模式中,管理者對象,主要是負(fù)責(zé)保存?zhèn)渫泴ο?/code>,這里有幾點(diǎn)要講一下。
并不一定要特別的做出一個(gè)管理者對象來
,廣義地說,調(diào)用原發(fā)器獲得備忘錄對象后,備忘錄對象放在哪里,哪個(gè)對象就可以算是管理者對象
。
管理者對象并不是只能管理一個(gè)備忘錄對象,一個(gè)管理者對象可以管理很多的備忘錄對象
,雖然前面的示例中是保存一個(gè)備忘錄對象,別忘了那只是個(gè)示意,并不是只能實(shí)現(xiàn)成那樣。
狹義的管理者對象,是只管理同一類的備忘錄對象,但是廣義管理者對象是可以管理不同類型的備忘錄對象的
。
管理者對象需要實(shí)現(xiàn)的基本功能主要就是:存入備忘錄對象、保存?zhèn)渫泴ο?、獲取備忘錄對象
,如果從功能上看,就是一個(gè)緩存功能的實(shí)現(xiàn),或者是一個(gè)簡單的對象實(shí)例池的實(shí)現(xiàn)
。
管理者雖然能存取備忘錄對象,但是不能訪問備忘錄對象內(nèi)部的數(shù)據(jù)
。
- 窄接口和寬接口
在備忘錄模式中,為了控制對備忘錄對象的訪問,出現(xiàn)了窄接口和寬接口的概念。
窄接口:管理者只能看到備忘錄的窄接口,
窄接口的實(shí)現(xiàn)里面通常沒有任何的方法,只是一個(gè)類型標(biāo)識(shí)
,窄接口使得管理者只能將備忘錄傳遞給其它對象。寬接口:
原發(fā)器能夠看到一個(gè)寬接口,允許它訪問所需的所有數(shù)據(jù),來返回到先前的狀態(tài)
。理想狀況是:只允許生成備忘錄的原發(fā)器來訪問該備忘錄的內(nèi)部狀態(tài),通常實(shí)現(xiàn)成為原發(fā)器內(nèi)的一個(gè)私有內(nèi)部類。
在前面的示例中,定義了一個(gè)名稱為FlowAMockMemento的接口,里面沒有定義任何方法,然后讓備忘錄來實(shí)現(xiàn)這個(gè)接口,從而標(biāo)識(shí)備忘錄就是這么一個(gè)FlowAMockMemento的類型,這個(gè)接口就是窄接口
。
在前面的實(shí)現(xiàn)中,備忘錄對象是實(shí)現(xiàn)在原發(fā)器內(nèi)的一個(gè)私有內(nèi)部類,只有原發(fā)器對象能訪問它,原發(fā)器可以訪問到備忘錄對象所有的內(nèi)部狀態(tài),這就是寬接口
。
這也算是備忘錄模式的標(biāo)準(zhǔn)實(shí)現(xiàn)方式,那就是窄接口沒有任何的方法,把備忘錄對象實(shí)現(xiàn)成為原發(fā)器對象的私有內(nèi)部類
。
那么能不能在窄接口里面提供備忘錄對象對外的方法,變相對外提供一個(gè)“寬”點(diǎn)的接口呢?
通常情況是不會(huì)這么做的,因?yàn)檫@樣一來,所有能拿到這個(gè)接口的對象就可以通過這個(gè)接口來訪問備忘錄內(nèi)部的數(shù)據(jù)或是功能,這違反了備忘錄模式的初衷,備忘錄模式要求“在不破壞封裝性的前提下”,如果這么做,那就等于是暴露了內(nèi)部細(xì)節(jié)
,因此,備忘錄模式在實(shí)現(xiàn)的時(shí)候,對外多是采用窄接口,而且通常不會(huì)定義任何方法。
- 使用備忘錄的潛在代價(jià)
標(biāo)準(zhǔn)的備忘錄模式的實(shí)現(xiàn)機(jī)制是依靠緩存來實(shí)現(xiàn)的
,因此,當(dāng)需要備忘的數(shù)據(jù)量較大時(shí),或者是存儲(chǔ)的備忘錄對象數(shù)據(jù)量不大但是數(shù)量很多的時(shí)候,或者是用戶很頻繁的創(chuàng)建備忘錄對象的時(shí)候,這些都會(huì)導(dǎo)致非常大的開銷。
因此在使用備忘錄模式的時(shí)候,一定要好好思考應(yīng)用的環(huán)境,如果使用的代價(jià)太高,就不要選用備忘錄模式,可以采用其它的替代方案。
- 增量存儲(chǔ)
如果需要頻繁的創(chuàng)建備忘錄對象,而且創(chuàng)建和應(yīng)用備忘錄對象來恢復(fù)狀態(tài)的順序是可控的,那么可以讓備忘錄進(jìn)行增量存儲(chǔ),也就是備忘錄可以僅僅存儲(chǔ)原發(fā)器內(nèi)部相對于上一次存儲(chǔ)狀態(tài)后的增量改變
。
比如:在命令模式實(shí)現(xiàn)可撤銷命令的實(shí)現(xiàn)中,就可以使用備忘錄來保存每個(gè)命令對應(yīng)的狀態(tài),然后在撤銷命令的時(shí)候,使用備忘錄來恢復(fù)這些狀態(tài)。由于命令的歷史列表是按照命令操作的順序來存放的,也是按照這個(gè)歷史列表來進(jìn)行取消和重做的,因此順序是可控的
。那么這種情況,還可以讓備忘錄對象只存儲(chǔ)一個(gè)命令所產(chǎn)生的增量改變而不是它所影響的每一個(gè)對象的完整狀態(tài)。
- 備忘錄模式調(diào)用順序示意圖
在使用備忘錄模式的時(shí)候,分成了兩個(gè)階段,第一個(gè)階段是創(chuàng)建備忘錄對象的階段,第二個(gè)階段是使用備忘錄對象來恢復(fù)原發(fā)器對象的狀態(tài)的階段
。它們的調(diào)用順序是不一樣的,下面分開用圖來示意一下。
先看創(chuàng)建備忘錄對象的階段,調(diào)用順序如圖所示:
再看看使用備忘錄對象來恢復(fù)原發(fā)器對象的狀態(tài)的階段,調(diào)用順序如圖所示:
3.2 結(jié)合原型模式##
在原發(fā)器對象創(chuàng)建備忘錄對象的時(shí)候,如果原發(fā)器對象中全部或者大部分的狀態(tài)都需要保存,一個(gè)簡潔的方式就是直接克隆一個(gè)原發(fā)器對象
。也就是說,這個(gè)時(shí)候備忘錄對象里面存放的是一個(gè)原發(fā)器對象的實(shí)例
。
還是通過示例來說明。只需要修改原發(fā)器對象就可以了,大致有如下變化:
首先原發(fā)器對象要實(shí)現(xiàn)可克隆的,好在這個(gè)原發(fā)器對象的狀態(tài)數(shù)據(jù)都很簡單,都是基本數(shù)據(jù)類型,所以直接用默認(rèn)的克隆方法就可以了,不用自己實(shí)現(xiàn)克隆,更不涉及深度克隆,否則,正確實(shí)現(xiàn)深度克隆還是個(gè)問題;
備忘錄對象的實(shí)現(xiàn)要修改,只需要存儲(chǔ)原發(fā)器對象克隆出來的實(shí)例對象就可以了;
相應(yīng)的創(chuàng)建和設(shè)置備忘錄對象的地方都要做修改;
示例代碼如下:
/**
* 模擬運(yùn)行流程A,只是一個(gè)示意,代指某個(gè)具體流程
*/
public class FlowAMockPrototype implements Cloneable {
private String flowName;
private int tempResult;
private String tempState;
public FlowAMockPrototype(String flowName){
this.flowName = flowName;
}
public void runPhaseOne(){
//在這個(gè)階段,可能產(chǎn)生了中間結(jié)果,示意一下
tempResult = 3;
tempState = "PhaseOne";
}
public void schema1(){
//示意,需要使用第一個(gè)階段產(chǎn)生的數(shù)據(jù)
this.tempState += ",Schema1";
System.out.println(this.tempState + " : now run "+tempResult);
this.tempResult += 11;
}
public void schema2(){
//示意,需要使用第一個(gè)階段產(chǎn)生的數(shù)據(jù)
this.tempState += ",Schema2";
System.out.println(this.tempState + " : now run "+tempResult);
this.tempResult += 22;
}
/**
* 創(chuàng)建保存原發(fā)器對象的狀態(tài)的備忘錄對象
* @return 創(chuàng)建好的備忘錄對象
*/
public FlowAMockMemento createMemento() {
try {
return new MementoImplPrototype((FlowAMockPrototype) this.clone());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
/**
* 重新設(shè)置原發(fā)器對象的狀態(tài),讓其回到備忘錄對象記錄的狀態(tài)
* @param memento 記錄有原發(fā)器狀態(tài)的備忘錄對象
*/
public void setMemento(FlowAMockMemento memento) {
MementoImplPrototype mementoImpl = (MementoImplPrototype)memento;
this.tempResult = mementoImpl.getFlowAMock().tempResult;
this.tempState = mementoImpl.getFlowAMock().tempState;
}
/**
* 真正的備忘錄對象,實(shí)現(xiàn)備忘錄窄接口,實(shí)現(xiàn)成私有的內(nèi)部類,不讓外部訪問
*/
private static class MementoImplPrototype implements FlowAMockMemento{
private FlowAMockPrototype flowAMock = null;
public MementoImplPrototype(FlowAMockPrototype f){
this.flowAMock = f;
}
public FlowAMockPrototype getFlowAMock() {
return flowAMock;
}
}
}
好了,結(jié)合原型模式來實(shí)現(xiàn)備忘錄模式的示例就寫好了,在前面的客戶測試程序中,創(chuàng)建原發(fā)器對象的時(shí)候,使用這個(gè)新實(shí)現(xiàn)的原發(fā)器對象就可以了。去測試和體會(huì)一下,看看是否能正確實(shí)現(xiàn)需要的功能。
不過要注意一點(diǎn),就是如果克隆對象非常復(fù)雜,或者需要很多層次的深度克隆,實(shí)現(xiàn)克隆的時(shí)候會(huì)比較麻煩
。
3.3 離線存儲(chǔ)##
標(biāo)準(zhǔn)的備忘錄模式,沒有討論離線存儲(chǔ)的實(shí)現(xiàn)。
事實(shí)上,從備忘錄模式的功能和實(shí)現(xiàn)上,是可以把備忘錄的數(shù)據(jù)實(shí)現(xiàn)成為離線存儲(chǔ)的,也就是不僅限于存儲(chǔ)于內(nèi)存中,可以把這些備忘數(shù)據(jù)存儲(chǔ)到文件中、xml中、數(shù)據(jù)庫中,從而支持跨越會(huì)話的備份和恢復(fù)功能
。
離線存儲(chǔ)甚至能幫助應(yīng)對應(yīng)用崩潰,然后關(guān)閉重啟的情況,應(yīng)用重啟過后,從離線存儲(chǔ)里面獲取相應(yīng)的數(shù)據(jù),然后重新設(shè)置狀態(tài),恢復(fù)到崩潰前的狀態(tài)。
當(dāng)然,并不是所有的備忘數(shù)據(jù)都需要離線存儲(chǔ),一般來講,需要存儲(chǔ)很長時(shí)間、或者需要支持跨越會(huì)話的備份和恢復(fù)功能、或者是希望系統(tǒng)關(guān)閉后還能被保存的備忘數(shù)據(jù),這些情況建議采用離線存儲(chǔ)。
離線存儲(chǔ)的實(shí)現(xiàn)也很簡單,就以前面模擬運(yùn)行流程的應(yīng)用來說,如果要實(shí)現(xiàn)離線存儲(chǔ),主要需要修改管理者對象,把它保存?zhèn)渫泴ο蟮姆椒?,?shí)現(xiàn)成為保存到文件中,而恢復(fù)備忘錄對象實(shí)現(xiàn)成為讀取文件就可以了。對于其它相關(guān)對象,主要是要實(shí)現(xiàn)序列化,只有可序列化的對象才能被存儲(chǔ)到文件中。
如果實(shí)現(xiàn)保存?zhèn)渫泴ο蟮轿募?,就不用在?nèi)存中保存了,去掉用來“記錄被保存的備忘錄對象”的這個(gè)屬性。示例代碼如下:
/**
* 負(fù)責(zé)在文件中保存模擬運(yùn)行流程A的對象的備忘錄對象
*/
public class FlowAMementoFileCareTaker {
/**
* 保存?zhèn)渫泴ο? * @param memento 被保存的備忘錄對象
*/
public void saveMemento(FlowAMockMemento memento){
//寫到文件中
ObjectOutputStream out = null;
try{
out = new ObjectOutputStream(
new BufferedOutputStream(
new FileOutputStream("FlowAMemento")
)
);
out.writeObject(memento);
}catch(Exception err){
err.printStackTrace();
}finally{
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 獲取被保存的備忘錄對象
* @return 被保存的備忘錄對象
*/
public FlowAMockMemento retriveMemento(){
FlowAMockMemento memento = null;
//從文件中獲取備忘錄數(shù)據(jù)
ObjectInputStream in = null;
try{
in = new ObjectInputStream(
new BufferedInputStream(
new FileInputStream("FlowAMemento")
)
);
memento = (FlowAMockMemento)in.readObject();
}catch(Exception err){
err.printStackTrace();
}finally{
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return memento;
}
}
同時(shí)需要讓備忘錄對象的窄接口繼承可序列化接口
,示例代碼如下:
/**
* 模擬運(yùn)行流程A的對象的備忘錄接口,是個(gè)窄接口
*/
public interface FlowAMockMemento extends Serializable {
}
還有FlowAMock對象,也需要實(shí)現(xiàn)可序列化示例代碼如下:
/**
* 模擬運(yùn)行流程A,只是一個(gè)示意,代指某個(gè)具體流程
*/
public class FlowAMock implements Serializable {
//中間的實(shí)現(xiàn)省略了
}
好了,保存到文件的存儲(chǔ)就實(shí)現(xiàn)好了,在前面的客戶測試程序中,創(chuàng)建管理者對象的時(shí)候,使用這個(gè)新實(shí)現(xiàn)的管理者對象就可以了。去測試和體會(huì)一下。
3.4 再次實(shí)現(xiàn)可撤銷操作##
在命令模式中,講到了可撤銷的操作,在那里講到:有兩種基本的思路來實(shí)現(xiàn)可撤銷的操作,一種是補(bǔ)償式或者反操作式
:比如被撤銷的操作是加的功能,那撤消的實(shí)現(xiàn)就變成減的功能;同理被撤銷的操作是打開的功能,那么撤銷的實(shí)現(xiàn)就變成關(guān)閉的功能。
另外一種方式是存儲(chǔ)恢復(fù)式
,意思就是把操作前的狀態(tài)記錄下來,然后要撤銷操作的時(shí)候就直接恢復(fù)回去就可以了。
這里就該來實(shí)現(xiàn)第二種方式,就是存儲(chǔ)恢復(fù)式,為了讓大家更好的理解可撤銷操作的功能,還是用原來的那個(gè)例子,對比學(xué)習(xí)會(huì)比較清楚。
這也相當(dāng)于是命令模式和備忘錄模式結(jié)合的一個(gè)例子,而且由于命令列表的存在,對應(yīng)保存的備忘錄對象也是多個(gè)
。
- 范例需求
考慮一個(gè)計(jì)算器的功能,最簡單的那種,只能實(shí)現(xiàn)加減法運(yùn)算,現(xiàn)在要讓這個(gè)計(jì)算器支持可撤銷的操作。
- 存儲(chǔ)恢復(fù)式的解決方案
存儲(chǔ)恢復(fù)式的實(shí)現(xiàn),可以使用備忘錄模式,大致實(shí)現(xiàn)的思路如下:
把原來的運(yùn)算類,就是那個(gè)Operation類,當(dāng)作原發(fā)器,原來的內(nèi)部狀態(tài)result,就只提供一個(gè)getter方法,來讓外部獲取運(yùn)算的結(jié)果;
在這個(gè)原發(fā)器里面,實(shí)現(xiàn)一個(gè)私有的備忘錄對象;
把原來的計(jì)算器類,就是Calculator類,當(dāng)作管理者,把命令對應(yīng)的備忘錄對象保存在這里。當(dāng)需要撤銷操作的時(shí)候,就把相應(yīng)的備忘錄對象設(shè)置回到原發(fā)器去,恢復(fù)原發(fā)器的狀態(tài);
(1)定義備忘錄對象的窄接口,示例代碼如下:
public interface Memento {
//空的
}
(2)定義命令的接口,有幾點(diǎn)修改:
修改原來的undo方法,傳入備忘錄對象
添加一個(gè)redo方法,傳入備忘錄對象
添加一個(gè)createMemento的方法,獲取需要被保存的備忘錄對象
示例代碼如下:
/**
* 定義一個(gè)命令的接口
*/
public interface Command {
/**
* 執(zhí)行命令
*/
public void execute();
/**
* 撤銷命令,恢復(fù)到備忘錄對象記錄的狀態(tài)
* @param m 備忘錄對象
*/
public void undo(Memento m);
/**
* 重做命令,恢復(fù)到備忘錄對象記錄的狀態(tài)
* @param m 備忘錄對象
*/
public void redo(Memento m);
/**
* 創(chuàng)建保存原發(fā)器對象的狀態(tài)的備忘錄對象
* @return 創(chuàng)建好的備忘錄對象
*/
public Memento createMemento();
}
(3)再來定義操作運(yùn)算的接口,相當(dāng)于計(jì)算器類這個(gè)原發(fā)器對外提供的接口,它需要做如下的調(diào)整:
去掉原有的setResult方法,內(nèi)部狀態(tài),不允許外部操作
添加一個(gè)createMemento的方法,獲取需要保存的備忘錄對象
添加一個(gè)setMemento的方法,來重新設(shè)置原發(fā)器對象的狀態(tài)
示例代碼如下:
/**
* 操作運(yùn)算的接口
*/
public interface OperationApi {
/**
* 獲取計(jì)算完成后的結(jié)果
* @return 計(jì)算完成后的結(jié)果
*/
public int getResult();
/**
* 執(zhí)行加法
* @param num 需要加的數(shù)
*/
public void add(int num);
/**
* 執(zhí)行減法
* @param num 需要減的數(shù)
*/
public void substract(int num);
/**
* 創(chuàng)建保存原發(fā)器對象的狀態(tài)的備忘錄對象
* @return 創(chuàng)建好的備忘錄對象
*/
public Memento createMemento();
/**
* 重新設(shè)置原發(fā)器對象的狀態(tài),讓其回到備忘錄對象記錄的狀態(tài)
* @param memento 記錄有原發(fā)器狀態(tài)的備忘錄對象
*/
public void setMemento(Memento memento);
}
(4)由于現(xiàn)在撤銷和恢復(fù)操作是通過使用備忘錄對象,直接來恢復(fù)原發(fā)器的狀態(tài)
,因此就不再需要按照操作類型來區(qū)分了,對于所有的命令實(shí)現(xiàn),它們的撤銷和重做都是一樣的。原來的實(shí)現(xiàn)是要區(qū)分的,如果是撤銷加的操作,那就是減,而撤銷減的操作,那就是加?,F(xiàn)在就不區(qū)分了,統(tǒng)一使用備忘錄對象來恢復(fù)。
因此,實(shí)現(xiàn)一個(gè)所有命令的公共對象,在里面把公共功能都實(shí)現(xiàn)了,這樣每個(gè)命令在實(shí)現(xiàn)的時(shí)候就簡單了
。順便把設(shè)置持有者的公共實(shí)現(xiàn)也放到這個(gè)公共對象里面來,這樣各個(gè)命令對象就不用再實(shí)現(xiàn)這個(gè)方法了,示例代碼如下:
/**
* 命令對象的公共對象,實(shí)現(xiàn)各個(gè)命令對象的公共方法
*/
public abstract class AbstractCommand implements Command{
/**
* 具體的功能實(shí)現(xiàn),這里不管
*/
public abstract void execute();
/**
* 持有真正的命令實(shí)現(xiàn)者對象
*/
protected OperationApi operation = null;
public void setOperation(OperationApi operation) {
this.operation = operation;
}
public Memento createMemento() {
return this.operation.createMemento();
}
public void redo(Memento m) {
this.operation.setMemento(m);
}
public void undo(Memento m) {
this.operation.setMemento(m);
}
}
(5)有了公共的命令實(shí)現(xiàn)對象,各個(gè)具體命令的實(shí)現(xiàn)就簡單了,實(shí)現(xiàn)加法命令的對象實(shí)現(xiàn),不再直接實(shí)現(xiàn)Command接口了,而是繼承命令的公共對象
,這樣只需要實(shí)現(xiàn)跟自己命令相關(guān)的業(yè)務(wù)方法就好了,示例代碼如下:
public class AddCommand extends AbstractCommand{
private int opeNum;
public AddCommand(int opeNum){
this.opeNum = opeNum;
}
public void execute() {
this.operation.add(opeNum);
}
}
看看減法命令的實(shí)現(xiàn),跟加法命令的實(shí)現(xiàn)差不多,示例代碼如下:
public class SubstractCommand extends AbstractCommand{
private int opeNum;
public SubstractCommand(int opeNum){
this.opeNum = opeNum;
}
public void execute() {
this.operation.substract(opeNum);
}
}
(6)接下來看看運(yùn)算類的實(shí)現(xiàn),相當(dāng)于是原發(fā)器對象,它的實(shí)現(xiàn)有如下改變:
不再提供setResult方法,內(nèi)部狀態(tài),不允許外部來操作
添加了createMemento和setMemento方法的實(shí)現(xiàn)
添加實(shí)現(xiàn)了一個(gè)私有的備忘錄對象
示例代碼如下:
/**
* 運(yùn)算類,真正實(shí)現(xiàn)加減法運(yùn)算
*/
public class Operation implements OperationApi{
/**
* 記錄運(yùn)算的結(jié)果
*/
private int result;
public int getResult() {
return result;
}
public void add(int num){
result += num;
}
public void substract(int num){
result -= num;
}
public Memento createMemento() {
MementoImpl m = new MementoImpl(result);
return m;
}
public void setMemento(Memento memento) {
MementoImpl m = (MementoImpl)memento;
this.result = m.getResult();
}
/**
* 備忘錄對象
*/
private static class MementoImpl implements Memento{
private int result = 0;
public MementoImpl(int result){
this.result = result;
}
public int getResult() {
return result;
}
}
}
(7)接下來該看看如何具體的使用備忘錄對象來實(shí)現(xiàn)撤銷操作和重做操作了。同樣在計(jì)算器類里面實(shí)現(xiàn),這個(gè)時(shí)候,計(jì)算器類就相當(dāng)于是備忘錄模式管理者對象。
實(shí)現(xiàn)思路:由于對于每個(gè)命令對象,撤銷和重做的狀態(tài)是不一樣的,撤銷是回到命令操作前的狀態(tài),而重做是回到命令操作后的狀態(tài)
,因此對每一個(gè)命令,使用一個(gè)備忘錄對象的數(shù)組來記錄對應(yīng)的狀態(tài)。
這些備忘錄對象是跟命令對象相對應(yīng)的,因此也跟命令歷史記錄一樣,設(shè)立相應(yīng)的歷史記錄,它的順序跟命令完全對應(yīng)起來
。在操作命令的歷史記錄的同時(shí),對應(yīng)操作相應(yīng)的備忘錄對象記錄。
示例代碼如下:
/**
* 計(jì)算器類,計(jì)算器上有加法按鈕、減法按鈕,還有撤銷和恢復(fù)的按鈕
*/
public class Calculator {
/**
* 命令的操作的歷史記錄,在撤銷時(shí)候用
*/
private List<Command> undoCmds = new ArrayList<Command>();
/**
* 命令被撤銷的歷史記錄,在恢復(fù)時(shí)候用
*/
private List<Command> redoCmds = new ArrayList<Command>();
/**
* 命令操作對應(yīng)的備忘錄對象的歷史記錄,在撤銷時(shí)候用,
* 數(shù)組有兩個(gè)元素,第一個(gè)是命令執(zhí)行前的狀態(tài),第二個(gè)是命令執(zhí)行后的狀態(tài)
*/
private List<Memento[]> undoMementos = new ArrayList<Memento[]>();
/**
* 被撤銷命令對應(yīng)的備忘錄對象的歷史記錄,在恢復(fù)時(shí)候用,
* 數(shù)組有兩個(gè)元素,第一個(gè)是命令執(zhí)行前的狀態(tài),第二個(gè)是命令執(zhí)行后的狀態(tài)
*/
private List<Memento[]> redoMementos = new ArrayList<Memento[]>();
private Command addCmd = null;
private Command substractCmd = null;
public void setAddCmd(Command addCmd) {
this.addCmd = addCmd;
}
public void setSubstractCmd(Command substractCmd) {
this.substractCmd = substractCmd;
}
public void addPressed(){
//獲取對應(yīng)的備忘錄對象,并保存在相應(yīng)的歷史記錄里面
Memento m1 = this.addCmd.createMemento();
//執(zhí)行命令
this.addCmd.execute();
//把操作記錄到歷史記錄里面
undoCmds.add(this.addCmd);
//獲取執(zhí)行命令后的備忘錄對象
Memento m2 = this.addCmd.createMemento();
//設(shè)置到撤銷的歷史記錄里面
this.undoMementos.add(new Memento[]{m1,m2});
}
public void substractPressed(){
//獲取對應(yīng)的備忘錄對象,并保存在相應(yīng)的歷史記錄里面
Memento m1 = this.substractCmd.createMemento();
//執(zhí)行命令
this.substractCmd.execute();
//把操作記錄到歷史記錄里面
undoCmds.add(this.substractCmd);
//獲取執(zhí)行命令后的備忘錄對象
Memento m2 = this.substractCmd.createMemento();
//設(shè)置到撤銷的歷史記錄里面
this.undoMementos.add(new Memento[]{m1,m2});
}
public void undoPressed(){
if(undoCmds.size()>0){
//取出最后一個(gè)命令來撤銷
Command cmd = undoCmds.get(undoCmds.size()-1);
//獲取對應(yīng)的備忘錄對象
Memento[] ms = undoMementos.get(undoCmds.size()-1);
//撤銷
cmd.undo(ms[0]);
//如果還有恢復(fù)的功能,那就把這個(gè)命令記錄到恢復(fù)的歷史記錄里面
redoCmds.add(cmd);
//把相應(yīng)的備忘錄對象也添加過去
redoMementos.add(ms);
//然后把最后一個(gè)命令刪除掉,
undoCmds.remove(cmd);
//把相應(yīng)的備忘錄對象也刪除掉
undoMementos.remove(ms);
}else{
System.out.println("很抱歉,沒有可撤銷的命令");
}
}
public void redoPressed(){
if(redoCmds.size()>0){
//取出最后一個(gè)命令來重做
Command cmd = redoCmds.get(redoCmds.size()-1);
//獲取對應(yīng)的備忘錄對象
Memento[] ms = redoMementos.get(redoCmds.size()-1);
//重做
cmd.redo(ms[1]);
//把這個(gè)命令記錄到可撤銷的歷史記錄里面
undoCmds.add(cmd);
//把相應(yīng)的備忘錄對象也添加過去
undoMementos.add(ms);
//然后把最后一個(gè)命令刪除掉
redoCmds.remove(cmd);
//把相應(yīng)的備忘錄對象也刪除掉
redoMementos.remove(ms);
}else{
System.out.println("很抱歉,沒有可恢復(fù)的命令");
}
}
}
(8)客戶端跟以前的實(shí)現(xiàn)沒有什么變化,示例代碼如下:
public class Client {
public static void main(String[] args) {
//1:組裝命令和接收者
//創(chuàng)建接收者
OperationApi operation = new Operation();
//創(chuàng)建命令
AddCommand addCmd = new AddCommand(5);
SubstractCommand substractCmd = new SubstractCommand(3);
//組裝命令和接收者
addCmd.setOperation(operation);
substractCmd.setOperation(operation);
//2:把命令設(shè)置到持有者,就是計(jì)算器里面
Calculator calculator = new Calculator();
calculator.setAddCmd(addCmd);
calculator.setSubstractCmd(substractCmd);
//3:模擬按下按鈕,測試一下
calculator.addPressed();
System.out.println("一次加法運(yùn)算后的結(jié)果為:" +operation.getResult());
calculator.substractPressed();
System.out.println("一次減法運(yùn)算后的結(jié)果為:" +operation.getResult());
//測試撤消
calculator.undoPressed();
System.out.println("撤銷一次后的結(jié)果為:" +operation.getResult());
calculator.undoPressed();
System.out.println("再撤銷一次后的結(jié)果為:" +operation.getResult());
//測試恢復(fù)
calculator.redoPressed();
System.out.println("恢復(fù)操作一次后的結(jié)果為:" +operation.getResult());
calculator.redoPressed();
System.out.println("再恢復(fù)操作一次后的結(jié)果為:" +operation.getResult());
}
}
運(yùn)行結(jié)果,示例如下:
一次加法運(yùn)算后的結(jié)果為:5
一次減法運(yùn)算后的結(jié)果為:2
撤銷一次后的結(jié)果為:5
再撤銷一次后的結(jié)果為:0
恢復(fù)操作一次后的結(jié)果為:5
再恢復(fù)操作一次后的結(jié)果為:2
跟前面采用補(bǔ)償式或者反操作式得到的結(jié)果是一樣的。好好體會(huì)一下,對比兩種實(shí)現(xiàn)方式,看看都是怎么實(shí)現(xiàn)的。順便也體會(huì)一下命令模式和備忘錄模式是如何結(jié)合起來實(shí)現(xiàn)功能的。
3.5 備忘錄模式的優(yōu)缺點(diǎn)##
- 更好的封裝性
備忘錄模式通過使用備忘錄對象,來封裝原發(fā)器對象的內(nèi)部狀態(tài)
,雖然這個(gè)對象是保存在原發(fā)器對象的外部,但是由于備忘錄對象的窄接口并不提供任何方法,這樣有效的保證了對原發(fā)器對象內(nèi)部狀態(tài)的封裝,不把原發(fā)器對象的內(nèi)部實(shí)現(xiàn)細(xì)節(jié)暴露給外部
。
- 簡化了原發(fā)器
備忘錄模式中,備忘錄對象被保存到原發(fā)器對象之外,讓客戶來管理他們請求的狀態(tài),從而讓原發(fā)器對象得到簡化。
- 窄接口和寬接口
備忘錄模式,通過引入窄接口和寬接口,使得不同的地方,對備忘錄對象的訪問是不一樣的。窄接口保證了只有原發(fā)器才可以訪問備忘錄對象的狀態(tài)
。
- 可能會(huì)導(dǎo)致高開銷
備忘錄模式基本的功能,就是對備忘錄對象的存儲(chǔ)和恢復(fù)
,它的基本實(shí)現(xiàn)方式就是緩存?zhèn)渫泴ο?。這樣一來,如果需要緩存的數(shù)據(jù)量很大,或者是特別頻繁的創(chuàng)建備忘錄對象,開銷是很大的。
3.6 思考備忘錄模式##
- 備忘錄模式的本質(zhì)
備忘錄模式的本質(zhì):保存和恢復(fù)內(nèi)部狀態(tài)。
保存是手段,恢復(fù)才是目的
,備忘錄模式備忘些什么東西呢?
就是原發(fā)器對象的內(nèi)部狀態(tài),備忘錄模式備忘的就是這些內(nèi)部狀態(tài),這些內(nèi)部狀態(tài)是不對外的,只有原發(fā)器對象才能夠進(jìn)行操作。
標(biāo)準(zhǔn)的備忘錄模式保存數(shù)據(jù)的手段是:通過內(nèi)存緩存,廣義的備忘錄模式實(shí)現(xiàn)的時(shí)候,可以采用離線存儲(chǔ)的方式,把這些數(shù)據(jù)保存到文件或者數(shù)據(jù)庫等地方
。
備忘錄模式為何要保存數(shù)據(jù)呢,目的就是為了在有需要的時(shí)候,恢復(fù)原發(fā)器對象的內(nèi)部狀態(tài),所以恢復(fù)是備忘錄模式的目的
。
根據(jù)備忘錄模式的本質(zhì),從廣義上講,進(jìn)行數(shù)據(jù)庫存取操作
;或者是web應(yīng)用中的request、session、servletContext等的attribute數(shù)據(jù)存取;更進(jìn)一步,大多數(shù)基于緩存功能的數(shù)據(jù)操作都可以視為廣義的備忘錄模式。不過廣義到這個(gè)地步,還提備忘錄模式已經(jīng)沒有什么意義了,所以對于備忘錄模式還是多從狹義上來說
。
事實(shí)上,對于備忘錄模式最主要的一個(gè)點(diǎn),就是封裝狀態(tài)的備忘錄對象,不應(yīng)該被除了原發(fā)器對象之外的對象訪問
,至于如何存儲(chǔ)那都是小事情。因?yàn)閭渫浤J揭鉀Q的主要問題就是:在不破壞對象封裝性的前提下,來保存和恢復(fù)對象的內(nèi)部狀態(tài)
。這是一個(gè)很主要的判斷點(diǎn),如果備忘錄對象可以讓原發(fā)器對象外的對象訪問的話,那就算是廣義的備忘錄模式了,其實(shí)提不提備忘錄模式已經(jīng)沒有太大的意義了
。
- 何時(shí)選用備忘錄模式
建議在如下情況中,選用備忘錄模式:
如果必須保存一個(gè)對象在某一個(gè)時(shí)刻的全部或者部分狀態(tài),這樣在以后需要的時(shí)候,可以把該對象恢復(fù)到先前的狀態(tài)
??梢允褂脗渫浤J剑褂脗渫泴ο髞矸庋b和保存需要保存的內(nèi)部狀態(tài),然后把備忘錄對象保存到管理者對象里面,在需要的時(shí)候,再從管理者對象里面獲取備忘錄對象,來恢復(fù)對象的狀態(tài)。
如果需要保存一個(gè)對象的內(nèi)部狀態(tài),但是如果用接口來讓其它對象直接得到這些需要保存的狀態(tài),將會(huì)暴露對象的實(shí)現(xiàn)細(xì)節(jié)并破壞對象的封裝性
??梢允褂脗渫浤J?,把備忘錄對象實(shí)現(xiàn)成為原發(fā)器對象的內(nèi)部類,而且還是私有的,從而保證只有原發(fā)器對象才能訪問該備忘錄對象。這樣既保存了需要保存的狀態(tài),又不會(huì)暴露原發(fā)器對象的內(nèi)部實(shí)現(xiàn)細(xì)節(jié)。
3.7 相關(guān)模式##
- 備忘錄模式和命令模式
這兩個(gè)模式可以組合使用。
命令模式實(shí)現(xiàn)中,在實(shí)現(xiàn)命令的撤銷和重做的時(shí)候,可以使用備忘錄模式
,在命令操作的時(shí)候記錄下操作前后的狀態(tài),然后在命令撤銷和重做的時(shí)候,直接使用相應(yīng)的備忘錄對象來恢復(fù)狀態(tài)就可以了。
在這種撤銷的執(zhí)行順序和重做執(zhí)行順序可控的情況下,備忘錄對象還可以采用增量式記錄的方式,可以減少緩存的數(shù)據(jù)量。
- 備忘錄模式和原型模式
這兩個(gè)模式可以組合使用。
在原發(fā)器對象創(chuàng)建備忘錄對象的時(shí)候,如果原發(fā)器對象中全部或者大部分的狀態(tài)都需要保存,一個(gè)簡潔的方式就是直接克隆一個(gè)原發(fā)器對象
。也就是說,這個(gè)時(shí)候備忘錄對象里面存放的是一個(gè)原發(fā)器對象的實(shí)例,這個(gè)在前面已經(jīng)示例過了,這里就不贅述了。