1. 程式人生 > >《大雄學設計模式--(2)備忘錄模式》

《大雄學設計模式--(2)備忘錄模式》

情景引入:

       傍晚夕陽的玫瑰色的餘暉鋪滿了大地,一切都是那麼美好。大雄一邊吹著口哨,一邊邁著輕快的步伐揹著書包進了家門。原來,這次大雄的數學破天荒考了個75分(要知道,以前他是經常不及格的)。為了表揚他,大雄媽媽也滿足了他的一個小心願,買了一個全新的掌上游戲機送給了他,也不忘叮囑他要再接再厲,不要沉迷遊戲,記得勞逸結合。高興的大雄拿起遊戲機馬上玩了一會,然後就往空地跑,一溜煙就不見了人影。。。


       原來,他打算來空地炫耀一番。但是,他剛到空地,就看到一群人圍著小夫,嘴上還嘰裡咕嚕說著什麼。大雄放慢了腳步,帶著好奇心悄悄地朝他們走了過去。原來,是小夫又靠他爸爸搞到了電視上剛剛推出的全新的高階掌上游戲機。這款遊戲機也不一般,不僅能夠對遊戲關卡進行存檔,而且能夠恢復至任意一個之前打過的關卡(該關卡的狀態包含了該關卡前面的所有關卡的狀態)再繼續打。大雄望了望小夫手裡正在操縱著的高階遊戲機,再瞥了一下自己的只能恢復到上一關卡的遊戲機,嘆了口氣,低著頭,悄悄地走開了。此時,夕陽的餘暉也差不多消失殆盡,整個街道籠罩上了一層特殊的色彩。。。

一、簡介

  • 備忘錄模式(Memento Pattern),又稱為快照模式(Snapshot Pattern)或Token模式,屬於行為型模式。
  • 在我們日常生活中的例子俯拾皆是:打Dota的存檔、影視劇中的“後悔藥”、下棋時雙方的悔棋、編寫Word時常按的Ctrl+Z、資料庫事務中的回滾操作等等。
  • 很多時候,我們總是需要記錄一個物件的內部狀態,這樣使得允許使用者取消不確定或錯誤的操作,並能夠恢復到他原先的狀態,使得其有“後悔藥”可以吃。
  • 對於相對頻繁而又簡單的恢復/撤銷操作並不需要存在磁碟中,只需要將儲存在記憶體中的狀態恢復一下即可,此時便可使用該模式。


二、具體內容

在不破壞封裝性的前提下,捕獲一個物件的內部狀態,並在該物件之外儲存這個狀態。這樣以後就可以將該物件恢復到原先儲存的狀態。

三、結構組成

其中客戶端不與備忘錄類耦合,而與備忘錄的管理者類耦合。

  • Originator(發起人類/角色)
    負責建立一個備忘錄Memento,用以儲存當前時刻自身的某些內部狀態,並可使用備忘錄恢復自身的內部狀態。它可以根據需要決定Memento儲存其自身的哪些內部狀態。

  • Memento(備忘錄類/角色)
    負責儲存Originator物件的內部狀態,並可防止Originator以外的其他物件訪問備忘錄Memento。它有兩個介面,分別為寬介面和窄介面。其中,Caretaker管理者只能看到備忘錄的窄介面(narrow interface),它只能將備忘錄傳遞給其他物件,而無法訪問備忘錄的內容;Originator發起人能看到備忘錄的寬介面(wide interface),允許它訪問備忘錄中返回到先前狀態所需的所有資料。

  • Caretaker(管理者類/角色)
    負責儲存好備忘錄Memento,不能對備忘錄中的內容進行操作或檢查。

四、UML類圖

1、”白箱”備忘錄模式

在這種模式中,備忘錄類對任何物件都提供一個寬介面,即備忘錄類的內部所儲存的狀態對所有物件都公開,故稱之為”白箱實現”。”白箱實現”中,將發起人類的狀態儲存在一個大家都看得到的地方,因此是破壞封裝性的。但是,程式設計師們通過自律,也是能在一定程度上實現該模式的大部分用意。因此”白箱實現”還是有意義的。
1

2、”黑箱”備忘錄模式

在這種模式中, 備忘錄類對發起人類物件提供一個寬介面,而對其他物件(包括Caretaker類)提供一個窄介面, 稱之為”黑箱實現”。在Java中,要實現雙重介面,可以將備忘錄類設計成為發起人類的內部成員類。”黑箱實現”中,將Memento類設定為Originator類的內部類,將Memento類(物件)封裝在Originator裡面,並在外部提供一個標識介面MementoIF(通常不含任何方法)給Caretaker以及其他物件。這樣一來, Originator類看到的是Memento類的所有介面,而Caretaker類以及其他物件看到的僅僅是標識介面MementoIF所暴露出來的介面。
2

3、具有多重檢查點的備忘錄模式

前面給出的”白箱”和”黑箱”實現都只是儲存一個狀態(即一個檢查點)的簡單實現。而在這種模式中,系統可以儲存多個檢查點(多個狀態)。具體來說,就是可以將發起人類物件的狀態儲存到備忘錄物件裡面,以後可以將發起人類物件恢復到備忘錄物件所儲存的某一個檢查點上。
3

4、”自述歷史”模式(History-On-Self Pattern)

是備忘錄模式的一個變種。在這種模式中,發起人角色兼任管理者角色。
4

五、情景例子的實現程式碼

將上面的情境用程式碼實現一番:

1、”白箱”備忘錄模式

//發起人類(普通掌上游戲機)
public class CommonPSP
{
    //關卡的狀態(包括多個屬性,如角色的攻擊力、防禦力...)
    private String stateOfGuanQia;

    //建立備忘錄(將當前關卡存檔)
    public Archive createArchive()
    {
        return new Archive(stateOfGuanQia);
    }

    //恢復備忘錄(通過存檔回到上一關卡)
    public void restoreArchive(Archive archive)
    {
        stateOfGuanQia = archive.getStateOfGuanQia();
    }


    public String getStateOfGuanQia()
    {
        return stateOfGuanQia;
    }

    public void setStateOfGuanQia(String stateOfGuanQia)
    {
        this.stateOfGuanQia = stateOfGuanQia;
    }
}

//備忘錄類(遊戲存檔)
public class Archive
{
    //關卡的狀態(包括多個屬性,如角色的攻擊力、防禦力...)
    private String stateOfGuanQia;

    public Archive(String stateOfGuanQia)
    {
        this.stateOfGuanQia = stateOfGuanQia;
    }


    public String getStateOfGuanQia()
    {
        return stateOfGuanQia;
    }

    public void setStateOfGuanQia(String stateOfGuanQia)
    {
        this.stateOfGuanQia = stateOfGuanQia;
    }
}


//管理者類(掌上游戲機中的存檔管理器)
public class ArchiveManager
{
    private Archive archive;

    //儲存遊戲存檔
    public void saveArchive(Archive archive)
    {
        this.archive = archive;
    }

    //獲取遊戲存檔
    public Archive retrieveArchive()
    {
        return archive;
    }

}

public class Client
{
    public static void main(String[] args)
    {
        CommonPSP commonPSP = new CommonPSP();
        ArchiveManager archiveManager = new ArchiveManager();

        //當前關卡的狀態
        commonPSP.setStateOfGuanQia("攻擊力:80,防禦力:100");

        //建立遊戲存檔,將當前關卡的狀態儲存到存檔中,並交由存檔管理器管理
        archiveManager.saveArchive(commonPSP.createArchive());

        //新的關卡的狀態
        commonPSP.setStateOfGuanQia("攻擊力:20,防禦力:30");

        //通過存檔管理器中的存檔恢復到上一個關卡的狀態
        commonPSP.restoreArchive(archiveManager.retrieveArchive());

        System.out.println("當前狀態為:" + commonPSP.getStateOfGuanQia());
    }

}

輸出結果

當前狀態為:攻擊力:80,防禦力:100

2、”黑箱”備忘錄模式

//發起人類(普通掌上游戲機)
public class CommonPSP
{
    //關卡的狀態(包括多個屬性,如角色的攻擊力、防禦力...)
    private String stateOfGuanQia;

    //建立備忘錄(將當前關卡存檔)
    public ArchiveIF createArchive()
    {
        return new Archive(stateOfGuanQia);
    }

    //恢復備忘錄(通過存檔回到上一關卡)
    public void restoreArchive(ArchiveIF archive)
    {
        stateOfGuanQia = ((Archive)archive).getStateOfGuanQia();
    }

    public String getStateOfGuanQia()
    {
        return stateOfGuanQia;
    }

    public void setStateOfGuanQia(String stateOfGuanQia)
    {
        this.stateOfGuanQia = stateOfGuanQia;
    }

    //備忘錄類(遊戲存檔)
    private class Archive implements ArchiveIF
    {
        //關卡的狀態(包括多個屬性,如角色的攻擊力、防禦力...)
        private String stateOfGuanQia;

        public Archive(String stateOfGuanQia)
        {
            this.stateOfGuanQia = stateOfGuanQia;
        }


        public String getStateOfGuanQia()
        {
            return stateOfGuanQia;
        }

        public void setStateOfGuanQia(String stateOfGuanQia)
        {
            this.stateOfGuanQia = stateOfGuanQia;
        }
    }

}

//窄介面,一個標識介面,沒有定義任何方法
public interface ArchiveIF
{
}

//管理者類(掌上游戲機中的存檔管理器)
public class ArchiveManager
{
    //由於存檔管理器拿到的是窄介面,故不可能改變遊戲存檔物件的內容
    private ArchiveIF archive;

    //儲存遊戲存檔
    public void saveArchive(ArchiveIF archive)
    {
        this.archive = archive;
    }

    //獲取遊戲存檔
    public ArchiveIF retrieveArchive()
    {
        return archive;
    }

}

public class Client
{
    public static void main(String[] args)
    {
        CommonPSP commonPSP = new CommonPSP();
        ArchiveManager archiveManager = new ArchiveManager();

        //當前關卡的狀態
        commonPSP.setStateOfGuanQia("攻擊力:80,防禦力:100");

        //建立遊戲存檔,將當前關卡的狀態儲存到存檔中,並交由存檔管理器管理
        archiveManager.saveArchive(commonPSP.createArchive());

        //新的關卡的狀態
        commonPSP.setStateOfGuanQia("攻擊力:20,防禦力:30");

        //通過存檔管理器中的存檔恢復到上一個關卡的狀態
        commonPSP.restoreArchive(archiveManager.retrieveArchive());

        System.out.println("當前狀態為:" + commonPSP.getStateOfGuanQia());
    }
}

輸出結果:

當前狀態為:攻擊力:80,防禦力:100

3、具有多重檢查點的備忘錄模式

import java.util.ArrayList;
import java.util.List;

//發起人類(高階掌上游戲機)
public class AdvancedPSP
{
    //當前關卡的狀態(該關卡的狀態由其前的所有關卡狀態決定)
    private List<String> statesOfGuanQias;

    //當前關卡的索引
    private int index;

    public AdvancedPSP()
    {
        statesOfGuanQias = new ArrayList<>();
        index = 0;
    }

    //將當前關卡存檔(建立備忘錄)
    public Archive createArchive()
    {
        return new Archive(statesOfGuanQias, index);
    }

    //通過存檔回到特定關卡(恢復備忘錄)
    public void restoreArchive(Archive archive)
    {
        statesOfGuanQias = archive.getStatesOfGuanQias();
        index = archive.getIndex();
    }


    public void setStateOfGuanQia(String stateOfGuanQia)
    {
        statesOfGuanQias.add(stateOfGuanQia);
        index++;
    }

    //輸出當前關卡的狀態(該關卡的狀態由其前的所有關卡狀態決定)
    public void printStatesOfGuanQias()
    {
        for(String stateOfGuanQia: statesOfGuanQias)
        {
            System.out.print(stateOfGuanQia + "    ");
        }
    }

}

import java.util.ArrayList;
import java.util.List;

//備忘錄類(遊戲存檔,一個遊戲存檔對應一個遊戲關卡的狀態,而關卡的狀態由其前的所有關卡狀態決定))
public class Archive
{
    //關卡的狀態
    private List<String> statesOfGuanQias;

    //關卡的索引
    private int index;

    public Archive(List<String> statesOfGuanQias, int index)
    {
        this.statesOfGuanQias = new ArrayList<String>(statesOfGuanQias);
        this.index = index;
    }

    //獲取當前存檔的關卡的狀態(該關卡的狀態由其前的所有關卡狀態決定)
    public List<String> getStatesOfGuanQias()
    {
        return statesOfGuanQias;
    }

    public int getIndex()
    {
        return index;
    }
}

import java.util.ArrayList;
import java.util.List;

//管理者類(掌上游戲機中的存檔管理器)
public class ArchiveManager
{
    private AdvancedPSP advancedPSP;
    private List<Archive> archives;
    //當前關卡(當前檢查點)
    private int curIndex;

    public ArchiveManager(AdvancedPSP advancedPSP)
    {
        this.advancedPSP = advancedPSP;
        this.archives = new ArrayList<>();
        this.curIndex = 0;
    }

    //建立一個遊戲存檔(建立一個檢查點)
    public int createArchive()
    {
        Archive archive = advancedPSP.createArchive();
        archives.add(archive);
        return curIndex++;
    }

    //恢復到某個關卡(恢復到某個檢查點)
    public void restoreArchive(int index)
    {
        Archive archive = archives.get(index);
        advancedPSP.restoreArchive(archive);
    }

    //將某個特定關卡的存檔刪除(刪除某個檢查點)
    public void removeArchive(int index)
    {
        archives.remove(index);
    }

}

public class Client
{
    public static void main(String[] args)
    {

        AdvancedPSP advancedPSP = new AdvancedPSP();
        ArchiveManager archiveManager = new ArchiveManager(advancedPSP);

        //當前關卡(關卡0)的狀態
        advancedPSP.setStateOfGuanQia("攻擊力:10,防禦力:20");

        //建立遊戲存檔(建立一個檢查點0)
        archiveManager.createArchive();

        //新的關卡(關卡1)的狀態
        advancedPSP.setStateOfGuanQia("攻擊力:20,防禦力:30");

        //建立遊戲存檔(建立一個檢查點1)
        archiveManager.createArchive();

        //新的關卡(關卡2)的狀態
        advancedPSP.setStateOfGuanQia("攻擊力:30,防禦力:40");

        //建立遊戲存檔(建立一個檢查點2)
        archiveManager.createArchive();

        //輸出當前關卡的狀態(該關卡的狀態由其前的所有關卡狀態決定)
        advancedPSP.printStatesOfGuanQias();

        //通過存檔管理器中的存檔恢復到關卡1的狀態(恢復到檢查點1)
        archiveManager.restoreArchive(1);

        System.out.println();

        //輸出當前關卡的狀態(該關卡的狀態由其前的所有關卡狀態決定)
        advancedPSP.printStatesOfGuanQias();

    }

}

輸出結果:

攻擊力:10,防禦力:20       攻擊力:20,防禦力:30       攻擊力:30,防禦力:40
攻擊力:10,防禦力:20       攻擊力:20,防禦力:30

4、”自述歷史”模式(History-On-Self Pattern)

//窄介面,一個標識介面,沒有定義任何方法
public interface ArchiveIF
{
}


//發起人類(普通掌上游戲機)兼任管理者類,負責儲存自己的備忘錄物件(遊戲存檔)
public class CommonPSP
{
    //關卡的狀態(包括多個屬性,如角色的攻擊力、防禦力...)
    private String stateOfGuanQia;

    //改變關卡的狀態
    public void changeStateOfGuanQia(String stateOfGuanQia)
    {
        this.stateOfGuanQia = stateOfGuanQia;
    }

    //建立備忘錄(將當前關卡存檔)
    public Archive createArchive()
    {
        return new Archive(this);
    }

    //恢復備忘錄(通過存檔回到上一關卡)
    public void restoreArchive(ArchiveIF archive)
    {
        changeStateOfGuanQia(((Archive)archive).stateOfGuanQia);
    }

    public String getStateOfGuanQia()
    {
        return stateOfGuanQia;
    }


    //備忘錄類(遊戲存檔)
    private class Archive implements ArchiveIF
    {
        //關卡的狀態(包括多個屬性,如角色的攻擊力、防禦力...)
        private String stateOfGuanQia;

        public Archive(CommonPSP commonPSP)
        {
            this.stateOfGuanQia = commonPSP.stateOfGuanQia;
        }

        public String getStateOfGuanQia()
        {
            return stateOfGuanQia;
        }

    }

}

public class Client
{
    public static void main(String[] args)
    {
        CommonPSP commonPSP = new CommonPSP();

        //當前關卡的狀態
        commonPSP.changeStateOfGuanQia("攻擊力:80,防禦力:100");

        //建立遊戲存檔
        ArchiveIF archive = commonPSP.createArchive();

        //新的關卡的狀態
        commonPSP.changeStateOfGuanQia("攻擊力:20,防禦力:30");

        //通過存檔恢復到上一個關卡的狀態
        commonPSP.restoreArchive(archive);

        System.out.println("當前狀態為:" + commonPSP.getStateOfGuanQia());
    }

}

輸出結果:

當前狀態為:攻擊力:80,防禦力:100

六、優點

  1. 提供了一種可以恢復狀態的機制。使使用者可以方便地回到之前的某個狀態。
  2. 實現了資訊的封裝。有時一些物件的內部資訊必須儲存在物件之外的地方,但是必須要由該物件自身來讀取,此時,使用備忘錄就可以把複雜的物件內部資訊對其他的物件遮蔽起來,從而恰當地保持了封裝地邊界,使得使用者/客戶端不需要關心物件內部狀態的儲存細節(怎麼儲存這些狀態,客戶端不用知道)。


七、缺點

  1. 狀態資料很耗記憶體資源。如果需要儲存的類的成員變數(狀態資料)過多,會佔據很大的記憶體。 如果客戶非常頻繁地建立備忘錄和恢復源發器狀態,可能會導致非常大的開銷。
  2. 操作開銷大。如果客戶端非常頻繁地建立備忘錄和恢復物件的內部狀態,可能會導致非常大的開銷。

八、什麼時候用

  1. 需要儲存/恢復資料的相關狀態場景。
  2. 用於功能比較複雜的,但需要維護或記錄屬性歷史的類,或者需要儲存的屬性只是眾多屬性中的一小部分。
  3. 如果在某個系統中使用命令模式時,需要實現命令的撤銷功能,那麼命令模式可以使用備忘錄模式來儲存可撤銷操作的狀態。
  4. 需要提供一個可回滾的操作時。


九、其他想說的

  • 管理者類的存在就是為了使得備忘錄模式符合迪米特原則。
  • 為了節約記憶體,可以考慮使用原型模式+備忘錄模式。