軟件構(gòu)造|備忘錄模式

一搓茬、備忘錄模式(Memento Pattern):

也稱為快照模式(Snapshot Pattern)23種設(shè)計(jì)模式之一娩贷,屬于行為模式

定義:在不破壞封閉的前提下颓屑,捕獲一個對象的內(nèi)部狀態(tài)粒褒,并在該對象之外保存這個狀態(tài)识颊。這樣以后就可將該對象恢復(fù)到原先保存的狀態(tài)。

二、備忘錄模式的結(jié)構(gòu)

涉及角色

? 發(fā)起人:負(fù)責(zé)創(chuàng)建一個備忘錄Memento祥款,用以記錄當(dāng)前時刻自身的內(nèi)部狀態(tài)清笨,并可使用備忘錄恢復(fù)內(nèi)部狀態(tài)。Originator可以根據(jù)需要決定Memento存儲自己的哪些內(nèi)部狀態(tài)刃跛。

? 備忘錄:負(fù)責(zé)存儲Originator對象的內(nèi)部狀態(tài)抠艾,并可以防止Originator以外的其他對象訪問備忘錄。備忘錄有兩個接口:Caretaker只能看到備忘錄的窄接口桨昙,他只能將備忘錄傳遞給其他對象检号。Originator卻可看到備忘錄的寬接口,允許它訪問返回到先前狀態(tài)所需要的所有數(shù)據(jù)蛙酪。

?? 為了控制對備忘錄對象的訪問齐苛,備忘錄模式中出現(xiàn)了窄接口寬接口的概念。

? ? ? ? ? ? 窄接口:管理者只能看到備忘錄的窄接口桂塞,窄接口的實(shí)現(xiàn)中通常沒有任何的方法凹蜂,只是一個類型標(biāo)識。窄接口使得管理者只能將備忘錄傳遞給其他對象阁危。

? ? ? ? ? ? 寬接口:原發(fā)器能夠看到備忘錄的寬接口玛痊,從而可以從備忘錄中獲取到所需的數(shù)據(jù),來將自己恢復(fù)到備忘錄中所保存的狀態(tài)狂打。理想情況是:只允許生成備忘錄的原發(fā)器來訪問該備忘錄的內(nèi)部狀態(tài)擂煞,通常實(shí)現(xiàn)成為原發(fā)器內(nèi)的一個私有內(nèi)部類。

? 管理者:負(fù)責(zé)備忘錄Memento趴乡,不能對Memento的內(nèi)容進(jìn)行訪問或者操作对省。


備忘錄模式結(jié)構(gòu)圖


簡單實(shí)現(xiàn)備忘錄的基本結(jié)構(gòu):

備忘錄窄接口定義:


發(fā)起者(Originator):通過創(chuàng)建一個新的備忘錄對象來保存自己的內(nèi)部狀態(tài) 示例代碼如圖:??

Originator


備忘錄管理者(Caretaker):負(fù)責(zé)保存?zhèn)渫泴ο螅菑牟恍薷模ㄉ踔敛徊榭矗﹤渫泴ο蟮膬?nèi)容浙宜。

備忘錄管理者

創(chuàng)建一個客戶端測試 示例代碼如圖:

測試

運(yùn)行程序打印結(jié)果如下:?

運(yùn)行結(jié)果


備忘錄模式具有封閉性,對于狀態(tài)得存儲只有發(fā)起者知道蛹磺。


三粟瞬、備忘錄模式的使用場景

? 需要保存和恢復(fù)數(shù)據(jù)的相關(guān)狀態(tài)場景。

? 提供一個可回滾(rollback)的操作萤捆。

? 數(shù)據(jù)庫連接的事務(wù)管理就是用的備忘錄模式裙品。

提出幾個實(shí)例方便我們理解:

?瀏覽器回退:瀏覽器一般有瀏覽記錄,當(dāng)我們在一個網(wǎng)頁上點(diǎn)擊幾次鏈接之后俗或,可在左上角點(diǎn)擊左箭頭回退到上一次的頁面市怎,然后也可以點(diǎn)擊右箭頭重新回到當(dāng)前頁面

? 數(shù)據(jù)庫備份與還原:一般的數(shù)據(jù)庫都支持備份與還原操作,備份即將當(dāng)前已有的數(shù)據(jù)或者記錄保留辛慰,還原即將已經(jīng)保留的數(shù)據(jù)恢復(fù)到對應(yīng)的表中

?編輯器撤銷與重做:在編輯器上編輯文字区匠,寫錯時可以按快捷鍵 Ctrl + z 撤銷,撤銷后可以按 Ctrl + y 重做

? 虛擬機(jī)生成快照與恢復(fù):虛擬機(jī)可以生成一個快照,當(dāng)虛擬機(jī)發(fā)生錯誤時可以恢復(fù)到快照的樣子

?Git版本管理:Git是最常見的版本管理軟件驰弄,每提交一個新版本麻汰,實(shí)際上Git就會把它們自動串成一條時間線,每個版本都有一個版本號戚篙,使用 git reset --hard 版本號 即可回到指定的版本五鲫,讓代碼時空穿梭回到過去某個歷史時刻

?棋牌游戲悔棋:在棋牌游戲中,有時下快了可以悔棋岔擂,回退到上一步重新下位喂。

示例1:在棋牌游戲中可以悔棋,即重新退回到上一步重新下棋乱灵。

棋子類 Chessman原發(fā)器角色:

@Data

@AllArgsConstructor

class Chessman {

? ? private String label;

? ? private int x;

? ? private int y;

? ? //保存狀態(tài)

? ? public ChessmanMemento save() {

? ? ? ? return new ChessmanMemento(this.label, this.x, this.y);

? ? }

? ? //恢復(fù)狀態(tài)

? ? public void restore(ChessmanMemento memento) {

? ? ? ? this.label = memento.getLabel();

? ? ? ? this.x = memento.getX();

? ? ? ? this.y = memento.getY();

? ? }

? ? public void show() {

? ? ? ? System.out.println(String.format("棋子<%s>:當(dāng)前位置為:<%d, %d>", this.getLabel(), this.getX(), this.getY()));

? ? }

}

備忘錄角色 ChessmanMemento:

@Data

@AllArgsConstructor

class ChessmanMemento {

? ? private String label;

? ? private int x;

? ? private int y;

}

負(fù)責(zé)人角色 MementoCaretaker:

class MementoCaretaker {

?? //定義一個集合來存儲備忘錄

? ? private ArrayList mementolist = new ArrayList();

? ? public ChessmanMemento getMemento(int i) {

? ? ? ? return (ChessmanMemento) mementolist.get(i);

? ? }

? ? public void addMemento(ChessmanMemento memento) {

? ? ? ? mementolist.add(memento);

? ? }

}

棋子客戶端塑崖,維護(hù)MementoCaretaker 對象:

class Client{

? ? private static int index = -1;

? ? private static MementoCaretaker mc = new MementoCaretaker();

? ? public static void main(String args[]) {

? ? ? ? Chessman chess = new Chessman("車", 1, 1);

? ? ? ? play(chess);

? ? ? ? chess.setY(4);

? ? ? ? play(chess);

? ? ? ? chess.setX(5);

? ? ? ? play(chess);

? ? ? ? undo(chess, index);

? ? ? ? undo(chess, index);

? ? ? ? redo(chess, index);

? ? ? ? redo(chess, index);

? ? }

? ? //下棋,同時保存?zhèn)渫?/p>

? ? public static void play(Chessman chess) {

? ? ? ? mc.addMemento(chess.save());

? ? ? ? index++;

? ? ? ? chess.show();

? ? }

? ? //悔棋阔蛉,撤銷到上一個備忘錄

? ? public static void undo(Chessman chess, int i) {

? ? ? ? System.out.println("******悔棋******");

? ? ? ? index--;

? ? ? ? chess.restore(mc.getMemento(i - 1));

? ? ? ? chess.show();

? ? }

? ? //撤銷悔棋弃舒,恢復(fù)到下一個備忘錄

? ? public static void redo(Chessman chess, int i) {

? ? ? ? System.out.println("******撤銷悔棋******");

? ? ? ? index++;

? ? ? ? chess.restore(mc.getMemento(i + 1));

? ? ? ? chess.show();

? ? }

}

運(yùn)行結(jié)果:

輸出如下,悔棋成功状原,撤銷悔棋成功

程序類圖:

程序類圖

示例2:備份電話本

聯(lián)系人:需要備份的數(shù)據(jù)聋呢,是狀態(tài)數(shù)據(jù),沒有操作

public sealed class ContactPerson

{

? ? //姓名? ? public string Name { get; set; }

? ? //電話號碼? ? public string MobileNumber { get; set; }

}

發(fā)起人:

public sealed class MobileBackOriginator

{

? ? // 發(fā)起人需要保存的內(nèi)部狀態(tài)? ? private List<ContactPerson> _personList;

? ? public List<ContactPerson> ContactPersonList

? ? {

? ? ? ? get? ? ? ? {

? ? ? ? ? ? return this._personList;

? ? ? ? }

? ? ? ? set? ? ? ? {

? ? ? ? ? ? this._personList = value;

? ? ? ? }

? ? }

? ? //初始化需要備份的電話名單? ? public MobileBackOriginator(List<ContactPerson> personList)

? ? {

? ? ? ? if (personList != null)

? ? ? ? {

? ? ? ? ? ? this._personList = personList;

? ? ? ? }

? ? ? ? else? ? ? ? {

? ? ? ? ? ? throw new ArgumentNullException("參數(shù)不能為空颠区!");

? ? ? ? }

? ? }

? ? // 創(chuàng)建備忘錄對象實(shí)例削锰,將當(dāng)期要保存的聯(lián)系人列表保存到備忘錄對象中? ? public ContactPersonMemento CreateMemento()

? ? {

? ? ? ? return new ContactPersonMemento(new List<ContactPerson>(this._personList));

? ? }

? ? // 將備忘錄中的數(shù)據(jù)備份還原到聯(lián)系人列表中? ? public void RestoreMemento(ContactPersonMemento memento)

? ? {

? ? ? ? this.ContactPersonList = memento.ContactPersonListBack;

? ? }

? ? public void Show()

? ? {

? ? ? ? Console.WriteLine("聯(lián)系人列表中共有{0}個人,他們是:", ContactPersonList.Count);

? ? ? ? foreach (ContactPerson p in ContactPersonList)

? ? ? ? {

? ? ? ? ? ? Console.WriteLine("姓名: {0} 號碼: {1}", p.Name, p.MobileNumber);

? ? ? ? }

? ? }

}

備忘錄對象:用于保存狀態(tài)數(shù)據(jù)毕莱,保存的是當(dāng)時對象具體狀態(tài)數(shù)據(jù)>>相當(dāng)于備忘錄角色(Memeto)

publicsealedclass ContactPersonMemento

{

? ? // 保存發(fā)起人創(chuàng)建的電話名單數(shù)據(jù)器贩,就是所謂的狀態(tài)publicList ContactPersonListBack {get;privateset; }

? ? publicContactPersonMemento(List personList)

? ? {

? ? ? ? ContactPersonListBack = personList;

? ? }

}

管理角色:它可以管理備忘錄對象,如果是保存多個備忘錄對象朋截,可以對保存的對象進(jìn)行增蛹稍、刪等管理處理>>相當(dāng)于管理者角色(Caretaker)

public sealed class MementoManager

{

? ? //如果想保存多個備忘錄對象,可以通過字典或者堆棧來保存部服,堆棧對象可以反映保存對象的先后順序

? ? //比如:public Dictionary<string, ContactPersonMemento> ContactPersonMementoDictionary { get; set; }? ? public ContactPersonMemento ContactPersonMemento { get; set; }

}

客戶端代碼:

class Program

{

? ? static void Main(string[] args)

? ? {

? ? ? ? List<ContactPerson> persons = new List<ContactPerson>()

? ? ? ? ? ? {

? ? ? ? ? ? ? ? new ContactPerson() { Name="小明", MobileNumber = "11111111"},

? ? ? ? ? ? ? ? new ContactPerson() { Name="小紅", MobileNumber = "22222222"},

? ? ? ? ? ? ? ? new ContactPerson() { Name="小華", MobileNumber = "33333333"}

? ? ? ? ? ? };

? ? ? ? //手機(jī)名單發(fā)起人? ? ? ? MobileBackOriginator mobileOriginator = new MobileBackOriginator(persons);

? ? ? ? mobileOriginator.Show();

? ? ? ? // 創(chuàng)建備忘錄并保存?zhèn)渫泴ο? ? ? ? MementoManager manager = new MementoManager();

? ? ? ? manager.ContactPersonMemento = mobileOriginator.CreateMemento();

? ? ? ? // 更改發(fā)起人聯(lián)系人列表? ? ? ? Console.WriteLine("----移除最后一個聯(lián)系人--------");

? ? ? ? mobileOriginator.ContactPersonList.RemoveAt(2);

? ? ? ? mobileOriginator.Show();

? ? ? ? // 恢復(fù)到原始狀態(tài)? ? ? ? Console.WriteLine("-------恢復(fù)聯(lián)系人列表------");

? ? ? ? mobileOriginator.RestoreMemento(manager.ContactPersonMemento);

? ? ? ? mobileOriginator.Show();

? ? ? ? Console.Read();

? ? }

}



唆姐、備忘錄模式的適用性

在以下條件下可以考慮使用備忘錄模式:

? ? ? ? ? ? 如果必須保存一個對象在某一個時刻的全部或部分狀態(tài),方便在以后需要的時候廓八,可以把該對象恢復(fù)到先前的狀態(tài)奉芦。

? ? ? ? ? ? 如果需要保存一個對象的內(nèi)部狀態(tài),但是如果用接口來讓其它對象直接得到這些需要保存的狀態(tài)剧蹂,將會暴露對象的實(shí)現(xiàn)細(xì)節(jié)并破壞對象的封裝性声功,這時可以使用備忘錄模式,把備忘錄對象實(shí)現(xiàn)成為原發(fā)器對象的私有內(nèi)部類宠叼,從而保證只有原發(fā)器對象才能訪問該備忘錄對象先巴。這樣既保存了需要保存的狀態(tài),又不會暴露原發(fā)器對象的內(nèi)部實(shí)現(xiàn)細(xì)節(jié)。 ?

主要優(yōu)點(diǎn)

?? 提供了一種可以恢復(fù)狀態(tài)的機(jī)制筹裕。當(dāng)用戶需要時能夠比較方便地將數(shù)據(jù)恢復(fù)到某個歷史的狀態(tài)醋闭。

?? 實(shí)現(xiàn)了內(nèi)部狀態(tài)的封裝。除了創(chuàng)建它的發(fā)起人之外朝卒,其他對象都不能夠訪問這些狀態(tài)信息证逻。

?? 簡化了發(fā)起人類。發(fā)起人不需要管理和保存其內(nèi)部狀態(tài)的各個備份抗斤,所有狀態(tài)信息都保存在備忘錄中囚企,并由管理者進(jìn)行管理,這符合單一職責(zé)原則瑞眼。

?? 更好的封裝性:備忘錄模式通過使用備忘錄對象龙宏,來封裝原發(fā)器對象的內(nèi)部狀態(tài),雖然這個對象是保存在原發(fā)器對象的外部伤疙,但是由于備忘錄對象的窄接口并不提供任何方法银酗。這樣有效地保證了對原發(fā)器對象內(nèi)部狀態(tài)的封裝,不把原發(fā)器對象的內(nèi)部實(shí)現(xiàn)細(xì)節(jié)暴露給外部徒像。

?? 簡化了原發(fā)器:在備忘錄模式中黍特,原發(fā)器不再需要管理和保存其內(nèi)部狀態(tài)的一個個版本,而是交由管理者或客戶端對這些狀態(tài)的版本進(jìn)行管理锯蛀,從而讓原發(fā)器對象得到簡化灭衷。

??? 窄接口和寬接口:備忘錄模式,通過引入窄接口和寬接口旁涤,使得不同的地方翔曲,對備忘錄對象的訪問是不一樣的。窄接口保證了只有原發(fā)器才可以訪問備忘錄對象存儲的狀態(tài)劈愚。

主要缺點(diǎn)

?? 資源消耗大瞳遍。如果要保存的內(nèi)部狀態(tài)信息過多或者特別頻繁,將會占用比較大的內(nèi)存資源菌羽。

?? 如果發(fā)起人角色的狀態(tài)需要完整地存儲到備忘錄對象中掠械,那么在資源消耗上面?zhèn)渫泴ο髸馨嘿F。

?? 當(dāng)負(fù)責(zé)人角色將一個備忘錄 存儲起來的時候算凿,負(fù)責(zé)人可能并不知道這個狀態(tài)會占用多大的存儲空間份蝴,從而無法提醒用戶一個操作是否很昂貴犁功。

?? 當(dāng)發(fā)起人角色的狀態(tài)改變的時候氓轰,有可能這個協(xié)議無效。如果狀態(tài)改變的成功率不高的話浸卦,不如采取“假如”協(xié)議模式署鸡。


五、備忘錄模式的實(shí)現(xiàn)

? ? ? ? 增量存儲: 如果需要頻繁地創(chuàng)建備忘錄對象,而且創(chuàng)建和應(yīng)用備忘錄對象來恢復(fù)狀態(tài)的順序是可控的靴庆,那么可以讓備忘錄進(jìn)行增量存儲时捌,也就是備忘錄可以僅僅存儲原發(fā)器內(nèi)部相對于上一次存儲狀態(tài)后的增量改變。

? ? ?? 結(jié)合原型模式:在原發(fā)器對象創(chuàng)建備忘錄對象的時候炉抒,如果原發(fā)器對象中全部或者大部分的狀態(tài)都需要保存奢讨,一個簡潔的方式就是直接克隆一個原發(fā)器對象。

? ?? ?? 離線存儲:備忘錄的數(shù)據(jù)可以實(shí)現(xiàn)成為離線存儲焰薄,除了存儲在內(nèi)存中拿诸,還可以把備忘錄數(shù)據(jù)存儲到文件中、XML中塞茅、數(shù)據(jù)庫中亩码,從而支持跨越會話的備份和恢復(fù)功能。 ?


六野瘦、總結(jié)

? ? ?? 備忘錄模式的功能描沟,首先是在不破壞封裝性的前提下,捕獲一個對象的內(nèi)部狀態(tài)鞭光。同時注意:一個是不破壞封裝性吏廉,即對象不能暴露它不應(yīng)該暴露的細(xì)節(jié);另外一個是捕獲的是對象的內(nèi)部狀態(tài)衰猛,而且通常還是運(yùn)行期間某個時刻對象的內(nèi)部狀態(tài)迟蜜。

? ? ?? 之所以要捕獲這些內(nèi)部狀態(tài),是為了在以后的某個時候啡省,可以將該對象的狀態(tài)恢復(fù)到備忘錄所保存的狀態(tài)娜睛,這才是備忘錄真正的目的。前面保存狀態(tài)就是為了后面恢復(fù)卦睹,雖然不是一定要恢復(fù)畦戒,但是目的是為了恢復(fù)。

? ? ? ? 在備忘錄模式中结序,備忘錄對象通常用來記錄原發(fā)器中需要保存的內(nèi)部狀態(tài)障斋,為了不破壞原發(fā)器對象的封裝性,一般只讓原發(fā)器自己來操作它的備忘錄對象徐鹤。為了保證這一點(diǎn)垃环,通常會把備忘錄對象作為原發(fā)器對象的內(nèi)部類來實(shí)現(xiàn),而且實(shí)現(xiàn)成私有的返敬,這樣就斷了外部來訪問這個備忘錄對象的途徑遂庄。?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市劲赠,隨后出現(xiàn)的幾起案子涛目,更是在濱河造成了極大的恐慌秸谢,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件霹肝,死亡現(xiàn)場離奇詭異估蹄,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)沫换,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進(jìn)店門臭蚁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人讯赏,你說我怎么就攤上這事刊棕。” “怎么了待逞?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵甥角,是天一觀的道長。 經(jīng)常有香客問我识樱,道長嗤无,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任怜庸,我火速辦了婚禮当犯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘割疾。我一直安慰自己嚎卫,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布宏榕。 她就那樣靜靜地躺著拓诸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪麻昼。 梳的紋絲不亂的頭發(fā)上奠支,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天,我揣著相機(jī)與錄音抚芦,去河邊找鬼倍谜。 笑死,一個胖子當(dāng)著我的面吹牛叉抡,可吹牛的內(nèi)容都是我干的尔崔。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼褥民,長吁一口氣:“原來是場噩夢啊……” “哼季春!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起轴捎,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤鹤盒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后侦副,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體侦锯,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年秦驯,在試婚紗的時候發(fā)現(xiàn)自己被綠了尺碰。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡译隘,死狀恐怖亲桥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情固耘,我是刑警寧澤题篷,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站厅目,受9級特大地震影響番枚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜损敷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一葫笼、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧拗馒,春花似錦路星、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至挥等,卻和暖如春垫挨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背触菜。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工九榔, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人涡相。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓哲泊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親催蝗。 傳聞我的和親對象是個殘疾皇子切威,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評論 2 355