保存快照和撤銷功能的實現方案——備忘錄模式總結

一、前言

本模式用的不是特別多,知道便可,本文主要是平時的讀書筆記的整理html

二、出現的動機和概念

備忘錄模式——也叫 Memo 模式,或者快照模式等,顧名思義就是實現歷史記錄的做用,好比能夠實現遊戲關卡的角色復活,任務進度保存,命令的撤銷,以及系統的快照留存記錄等功能。java

備忘錄模式的用意是在不破壞封裝的條件下,將一個對象的狀態捕捉(Capture),並外部化存儲,從而能夠在未來合適的時候把這個對象還原到存儲時的狀態(undo/rollback)。git

很簡單的概念,能夠聯繫Git,還有數據庫事務處理等,它們都有版本記錄,操做回滾的邏輯,這些均可以基於備忘錄模式,搭配其餘模式來優雅的實現。數據庫

一句話:當業務需求是讓對象返回以前的某個歷史性的狀態的時候,就應該使用備忘錄模式加以封裝。數組

2.一、什麼叫破壞了封裝性

假如保存某個對象 A 的當前狀態 A1,那麼 RD 天然不是撐得沒事幹,確定是爲了將來能回滾或者查看對象 A 的這個當前狀態 A1。天然的,外部的類(對象)就必定要可以自由的訪問 A 的內部狀態(即有一段代碼 B 須要依賴 A 的內部結構),不然連保存什麼都不知道,那還保存個什麼勁兒呢。那麼問題來了,若是稍不注意,就會把 B 分散在系統的各個角落,致使系統對 A 恢復操做的管理日益雜亂,增大開發和維護成本。這就叫破壞了封裝性。安全

2.二、如何防止關鍵對象的封裝性遭到破壞

答案很明顯,就是使用備忘錄模式加以設計。app

三、由投色子游戲引出

遊戲會有玩家復活功能,或者關卡進度恢復的功能,若是你不提供這樣的功能,確定沒人玩。可是遊戲的狀態是很是關鍵的數據,必需要封裝得當,不能讓別人隨意訪問。dom

下面看一個投色子的遊戲機例子,玩家先充錢(200起步)才能玩,且按下投擲按鈕,讓機器來搖色子:ide

一、點數爲1,玩家贏100塊錢post

二、爲2,輸200塊錢

三、爲6,玩家不贏錢,可是能夠獲得一個禮物,禮物裏分爲兩類:

  • 記念意義的禮物,不值錢

  • 能夠積累換積分,兌換錢的vip禮物

四、玩家沒錢了,遊戲結束

五、若是玩家不想結束當前遊戲,則能夠充錢恢復到最初狀態。

咱們用 User 表明玩家,Memo 表明備忘錄,Game 表明遊戲機。

3.一、備忘錄類和單一職責原則

Memo 表明備忘錄類,是備忘錄模式的核心類。顧名思義,它只有一個功能——負責保存和恢復目標對象的狀態,好比建立快照,恢復快照。而到底何時建立快照,何時恢復快照,Memo 類並不關心。

在例子中,Memo 表示玩家的狀態,注意該類和表明玩家類的類(User)都必須在一個包下面。

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

/**
 * 特別要注意,各個屬性和方法的 包 權限,它和用戶類需在一個包下
 */
public class Memo {
    /**
     * 表明用戶的錢,爲包訪問權限
     */
    int money;

    /**
     * 表明用戶的禮物,爲包訪問權限
     */
    ArrayList gifts;

    /**
     * 包訪問權限的構造器——這是一個寬接口
     */
    Memo(int money) {
        this.money = money;
        this.gifts = new ArrayList<>();
    }

    // 窄接口,獲取用戶的當前狀態下的錢
    public int getMoney() {
        return money;
    }

    /**
     * 寬接口,保存禮物
     */
    void addGift(String gift) {
        this.gifts.add(gift);
    }

    /**
     * 寬接口,獲取當前用戶持有的全部禮物
     */
    List getGifts() {
        return (List) this.gifts.clone();
    }
}

必定注意 Memo 類的成員權限,這很是重要:

一、構造器在包外沒法被訪問,只有本包內的類能夠訪問,生成 Memo 實例

二、addGift 方法也是隻有同一個包下的類能訪問——給用戶保存所得的禮物,外部包的類沒法改變 Memo(備忘錄)的數據

三、只有 getMoney 是 public,雖然只有它能被外界隨意訪問,但叫窄接口

3.1.一、Java 類成員的訪問權限

權限

訪問限制的說明

public

任何類

protected

同一個包的類,或者該類的子類

無,也叫默認權限,或者包權限

同一個包的類

private

該類本身

3.1.二、寬接口和窄接口

所謂的寬,窄,要明白針對誰說的——它們都是面向的備忘錄 Memo,即寬接口是說其餘類調用了該方法,那麼就能得到 or 修改 Memo 的全部快照中的數據,這就是所謂的寬的意思。而窄接口,是說其餘類調用了該方法,那麼只能得到當前快照中的數據,這就是所謂的窄。

在 Memo 類中,只有 public int getMoney() 方法是窄接口,只有它能夠被外部的類訪問,而修改狀態的寬接口們,不能夠被外部的類訪問。

3.1.三、Java 拷貝

Memo 類裏,對 List getGifts 方法的返回值進行了 clone,其中,ArrayList 默認給重寫了 clone 方法,可是是淺拷貝的,須要注意。

/**
 * Returns a shallow copy of this <tt>ArrayList</tt> instance.  (The
 * elements themselves are not copied.)
 *
 * @return a clone of this <tt>ArrayList</tt> instance
 */
public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

3.二、和 Memo 類同包的用戶類 User(生成者類)

User類——須要被保存狀態以便恢復的那個對象。而如何恢復和保存快照的邏輯,它不 care,是前面的 Memo 類負責。

簡單的規則就是,只要玩家沒有輸光了錢,它就能夠一直玩下去

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

public class User {
    private static final String[] GIFT_NAME = {"手機", "掃地機器人", "圓珠筆"};
    private Random random = new Random();
    private int money; // 玩家的錢
    private List gifts = new ArrayList<>();

    public User(int money) {
        this.money = money;
    }

    public int getMoney() {
        return money;
    }

    public void playGame() {
        int dice = random.nextInt(6) + 1; // [1-6]
        switch (dice) {
            case 1:
                this.money += 100;
                System.out.println("money + 100");
                break;
            case 2:
                this.money -= 200;
                System.out.println("money -100");
                break;
            case 6:
                String gift = getGift();
                System.out.println("gitf is " + gift);
                break;
            default:
                System.out.println("平局");
        }
    }

    // 保存快照
    public Memo captureState() {
        // 保存當前餘額
        Memo memo = new Memo(this.money);
        Iterator iterator = this.gifts.iterator(); // 保存當前所有禮物(只保存 VIP 禮物)
        while (iterator.hasNext()) {
            String g = (String) iterator.next();
            if (g.startsWith("VIP")) {
                memo.addGift(g);
            }
        }
        return memo;
    }

    // 恢復快照
    public void restoreState(Memo memo) {
        this.money = memo.getMoney();
        this.gifts = memo.getGifts();
    }

    // 模擬隨機生成一個禮物給用戶,該方法不該該放這裏的,爲了演示
    private String getGift() {
        String prefix = "";
        if (random.nextBoolean()) {
            prefix = "VIP: ";
        }
        return prefix + GIFT_NAME[random.nextInt(GIFT_NAME.length)];
    }

    @Override
    public String toString() {
        return "[ money = " + this.money + ", gifts = " + this.gifts + " ]";
    }
}

captureState 方法用來保存玩家的當前狀態(拍攝快照),並把快照返回給調用者,這個調用者就是接下來要實現的管理類。類比拍照,captureState 方法拍下當前玩家的快照,並保存到 Memo 類中。

restoreState 相反就是撤銷(回滾,恢復)的操做。

3.三、包外的管理者類 Main——什麼時候保存/恢復快照

Main 類會初始化一個 User 實例,表明一個玩家,在玩家遊戲的過程當中,由 Main 類決定什麼時候保存 User 快照,什麼時候恢復 User 快照。具體的保存和恢復策略以及存儲的位置,是 Memo 這個備忘錄類實現的。

若是玩家運氣好,會贏錢(禮物),並保存當前快照以便於將來恢復到這個狀態。若是運氣很差,輸光了,玩家會立刻買籌碼,此時系統自動調用恢復快照的方法,讓玩家恢復到死亡以前的狀態。

import com.dashuai.D10Memo.memo.Memo;
import com.dashuai.D10Memo.memo.User;

public class Main {
    public static void main(String[] args) {
        // 玩家開始遊戲,初始化一個用戶實例,表明該玩家
        User user = new User(100);
        System.out.println("玩家111,買了100籌碼,開始遊戲");
        // 保存玩家初始化的狀態,這是最先的恢復點
        Memo memo = user.captureState();
for (int i = 1; i <= 10; i++) { System.out.println("用戶的當前狀態:" + user); System.out.println("------第 " + i + " 局"); user.playGame(); System.out.println("該局結束後,當前用戶的金錢 = " + user.getMoney()); if (user.getMoney() > memo.getMoney()) { System.out.println("贏了不少啊,值得保存一下游戲進度"); memo = user.captureState(); System.out.println("保存完畢!"); } else if (user.getMoney() <= 0) { System.out.println("輸光了,復活時間內,用戶立刻買籌碼,爲其復活,恢復到遊戲結束前的狀態"); user.restoreState(memo); } } } }玩家111,買了100籌碼,開始遊戲 用戶的當前狀態:[ money = 100, gifts = [] ] ------第 1 局 平局 該局結束後,當前用戶的金錢 = 100 用戶的當前狀態:[ money = 100, gifts = [] ] ------第 2 局 平局 該局結束後,當前用戶的金錢 = 100 用戶的當前狀態:[ money = 100, gifts = [] ] ------第 3 局 平局 該局結束後,當前用戶的金錢 = 100 用戶的當前狀態:[ money = 100, gifts = [] ] ------第 4 局 平局 該局結束後,當前用戶的金錢 = 100 用戶的當前狀態:[ money = 100, gifts = [] ] ------第 5 局 money + 100 該局結束後,當前用戶的金錢 = 200 贏了不少啊,值得保存一下游戲進度 保存完畢! 用戶的當前狀態:[ money = 200, gifts = [] ] ------第 6 局 平局 該局結束後,當前用戶的金錢 = 200 用戶的當前狀態:[ money = 200, gifts = [] ] ------第 7 局 money -200 該局結束後,當前用戶的金錢 = 0 輸光了,復活時間內,用戶立刻買籌碼,爲其復活,恢復到遊戲結束前的狀態 用戶的當前狀態:[ money = 200, gifts = [] ] ------第 8 局 gitf is VIP: 圓珠筆 該局結束後,當前用戶的金錢 = 200 用戶的當前狀態:[ money = 200, gifts = [] ] ------第 9 局 平局 該局結束後,當前用戶的金錢 = 200 用戶的當前狀態:[ money = 200, gifts = [] ] ------第 10 局 gitf is VIP: 手機 該局結束後,當前用戶的金錢 = 200

一、Main 做爲包外的類,就是所謂的管理者類,它管理 User(生成者類) 和 Memo (備忘錄類),前者用來表示要保存的對象,後者表示如何保存的邏輯和保存的地點。

二、因爲管理者類Main在包外,故 Main 不能直接訪問 Memo 類的構造器,沒法直接生成備忘錄,保證了備忘錄的封裝完整,Main 只能經過調用 User 類的 public 的 getMoney 方法獲取當前玩家的金錢,不能隨意改變玩家的餘額,保證了安全性。

三、由管理者類——Main 決定,什麼時候拍攝玩家的快照或者什麼時候恢復這個快照。具體的拍照和恢復策略,是 Memo——備忘錄類自己實現。

四、備忘錄模式的標準類圖和角色

一、生成者——對應了示例的 User 類,是須要被保存狀態以便恢復的那個對象

二、備忘錄——Memo 類,該對象由生成者建立,主要用來保存生成者的內部狀態

三、管理者——Main 類,負責管理在適當的時間保存/恢復生成者對象的狀態。

備忘錄角色有以下責任:

1)將生成者對象的內戰狀態存儲。備忘錄能夠根據生成者對象的狀態判斷來決定存儲多少生成者對象的內部狀態

2)備忘錄能夠保護其內容不被生成者對象以外的任何對象所讀取

五、備忘錄模式的應用場景

若是一個對象須要保存狀態並可經過undo或rollback等操做恢復到之前的狀態時,可使用Memento模式

具體說,若是一個類須要保存它的對象的狀態(至關於生成者角色),能夠

一、設計一個類,該類只是用來保存上述對象的狀態(至關於 Memo 角色)

二、須要的時候,管理者角色要求生成者返回一個Memo並加以保存

三、undo或rollback時,經過管理者保存的Memo對象,恢復生成者的狀態

六、多個備忘錄的情景

以前的例子,Main 這個管理者類只保存了一個 memo,若是在Main集成數組或者list,則能夠實現歷史訪問點的快照,便於恢復各個時間點的狀態。

七、Memo 備忘錄類的有效期問題

若是在內存中保存 memo,那麼程序結束,就沒用了。此時可把 memo 保存到數據庫或者文件裏序列化。可是到底保存多久又是個新問題,須要結合具體業務涉及。

八、劃分管理者和生成者角色的意義

爲何要這麼麻煩呢,直接所有實如今備忘錄類不得了麼。

由於,管理者角色的職責是決定什麼時候保存生成者的快照,什麼時候撤銷。另外一方面,生成者角色的職責是表明被保存和恢復的那個對象,他生成備忘錄角色對象和使用接受到的備忘錄角色對象恢復本身的狀態。

這樣就實現了職責分離。以下當需求變更:

一、撤銷一次的操做,變動爲撤銷屢次時

二、變動拍攝快照保存到內存爲保存到數據庫,or 文件時

都不須要反覆修改生成者角色的代碼了,這個生成者是實現關鍵業務邏輯的類,保證封裝的穩定性。

九、常常和備忘錄模式搭配的其餘模式

備忘錄模式經常與命令模式和迭代模式一同使用。好比命令模式實現撤銷操做,能夠搭配備忘錄模式

十、備忘錄模式的優缺點

一、優勢

把被存儲的狀態放在外面——Memo角色,不和關鍵對象(生成者角色)混在一塊兒,維護了各自的內聚和封裝性。

能提供快照和恢復功能

二、缺點

若是鏈接數據庫或者文件,可能拍攝快照和恢復的動做比較耗時

十一、序列化和備忘錄模式

Java中,能使用序列化機制實現對象的狀態保存,所以能夠搭配序列化機制實現備忘錄模式,參看:Java對象序列化全面總結

相關文章
相關標籤/搜索