「補課」進行時:設計模式(17)——備忘錄模式

1. 前文彙總

「補課」進行時:設計模式系列

2. 從版本控制開始

相信每個程序猿,每天工作都會使用版本控制工具,不管是微軟提供的 vss 還是 tfs ,又或者是開源的 svn 或者 git ,每天下班前,總歸會使用版本控制工具提交一版代碼。

版本管理工具是讓我們在代碼出問題的時候,可以方便的獲取到之前的版本進行版本回退,尤其是在項目發佈投運的時候,當出現問題的時候直接獲取上一個版本進行回滾操作。

在這個操作中間,最重要的就是保存之前的狀態,那麼如何保存之前的狀態?

操作很簡單,我們可以定義一箇中間變量,保留這個原始狀態。

先定義一個版本管理 Git 類:

public class Git {
    private String state;
    // 版本發生改變,現在是 version2
    public void changeState() {
        this.state = "version2";
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }
}

然後是一個場景 Client 類:

public class Client {
    public static void main(String[] args) {
        Git git = new Git();
        // 初始化版本
        git.setState("version1");
        System.out.println("當前的版本信息:");
        System.out.println(git.getState());
        // 記錄下當前的狀態
        Git backup = new Git();
        backup.setState(git.getState());
        // 提交一個版本,版本進行改變
        git.changeState();
        System.out.println("提交一個版本後的版本信息:");
        System.out.println(git.getState());
        // 回退一個版本,版本信息回滾
        git.setState(backup.getState());
        System.out.println("回退一個版本後的版本信息:");
        System.out.println(git.getState());
    }
}

執行結果:

當前的版本信息:
version1
提交一個版本後的版本信息:
version2
回退一個版本後的版本信息:
version1

程序運行正確,輸出結果也是我們期望的,但是結果正確並不表示程序是合適的。

在場景類 Client 類中,這個是高層模塊,現在卻在高層模塊中做了中間臨時變量 backup 的狀態的保持,爲什麼一個狀態的保存和恢復要讓高層模塊來負責呢?

這個中間臨時變量 backup 應該是 Git 類的職責,而不是讓一個高層次的模塊來進行定義。

我們新建一個 Memento 類,用作負責狀態的保存和備份。

public class Memento {
    private String state;
    
    public Memento(String state) {
        this.state = state;
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }
}

新建一個 Memento ,用構造函數來傳遞狀態 state ,修改上面的 Git 類,新增兩個方法 createMemento()restoreMemento(),用來創建備忘錄以及恢復一個備忘錄。

public class Git {
    private String state;
    // 版本發生改變,現在是 version2
    public void changeState() {
        this.state = "version2";
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }
    // 創建一個備忘錄
    public Memento createMemento(String state) {
        return new Memento(state);
    }
    // 恢復一個備忘錄
    public void restoreMemento(Memento memento) {
        this.setState(memento.getState());
    }
}

修改後的場景類:

public class Client {
    public static void main(String[] args) {
        Git git = new Git();
        // 初始化版本
        git.setState("version1");
        System.out.println("當前的版本信息:");
        System.out.println(git.getState());
        // 記錄下當前的狀態
        Memento mem = git.createMemento(git.getState());
        // 提交一個版本,版本進行改變
        git.changeState();
        System.out.println("提交一個版本後的版本信息:");
        System.out.println(git.getState());
        // 項目發佈失敗,回滾狀態
        git.restoreMemento(mem);
        System.out.println("回退一個版本後的版本信息:");
        System.out.println(git.getState());
    }
}

運行結果和之前的案例保持一致,那麼這就結束了麼,當然沒有,雖然我們在 Client 中不再需要重複定義 Git 類了,但是這是對迪米特法則的一個褻瀆,它告訴我們只和朋友類通信,那這個備忘錄對象是我們必須要通信的朋友類嗎?對高層模塊來說,它最希望要做的就是創建一個備份點,然後在需要的時候再恢復到這個備份點就成了,它不用關心到底有沒有備忘錄這個類。

那我們可以對這個備忘錄的類再做一下包裝,創建一個管理類,專門用作管理這個備忘錄:

public class Caretaker {
    private Memento memento;

    public Memento getMemento() {
        return memento;
    }

    public void setMemento(Memento memento) {
        this.memento = memento;
    }
}

非常簡單純粹的一個 JavaBean ,甭管它多簡單,只要有用就成,我們來看場景類如何調用:

public class Client {
    public static void main(String[] args) {
        Git git = new Git();
        // 創建一個備忘錄管理者
        Caretaker caretaker = new Caretaker();
        // 初始化版本
        git.setState("version1");
        System.out.println("當前的版本信息:");
        System.out.println(git.getState());
        // 記錄下當前的狀態
        caretaker.setMemento(git.createMemento(git.getState()));
        // 提交一個版本,版本進行改變
        git.changeState();
        System.out.println("提交一個版本後的版本信息:");
        System.out.println(git.getState());
        // 項目發佈失敗,回滾狀態
        git.restoreMemento(caretaker.getMemento());
        System.out.println("回退一個版本後的版本信息:");
        System.out.println(git.getState());
    }
}

現在這個備份者就類似於一個備份的倉庫管理員,創建一個丟進去,需要的時候再拿出來。這就是備忘錄模式。

3. 備忘錄模式

3.1 定義

備忘錄模式(Memento Pattern)提供了一種彌補真實世界缺陷的方法,讓“後悔藥”在程序的世界中真實可行,其定義如下:

Without violating encapsulation,capture and externalize an object's internalstate so that the object can be restored to this state later.(在不破壞封裝性的前提下,捕獲一個對象的內部狀態,並在該對象之外保存這個狀態。這樣以後就可將該對象恢復到原先保存的狀態。)

3.2 通用類圖

  • Originator 發起人角色:記錄當前時刻的內部狀態,負責定義哪些屬於備份範圍的狀態,負責創建和恢復備忘錄數據。
  • Memento 備忘錄角色:負責存儲 Originator 發起人對象的內部狀態,在需要的時候提供發起人需要的內部狀態。
  • Caretaker 備忘錄管理員角色:對備忘錄進行管理、保存和提供備忘錄。

3.3 通用代碼

發起人:

public class Originator {
    private String state;

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }
    // 創建一個備忘錄
    public Memento createMemento() {
        return new Memento(this.state);
    }
    // 恢復一個備忘錄
    public void restoreMemento(Memento memento) {
        this.setState(memento.getState());
    }
}

備忘錄:

public class Memento {
    private String state;
    public Memento(String state) {
        this.state = state;
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }
}

備忘錄管理員:

public class Caretaker {
    // 備忘錄對象
    private Memento memento;

    public Memento getMemento() {
        return memento;
    }

    public void setMemento(Memento memento) {
        this.memento = memento;
    }
}

場景類:

public class Client {
    public static void main(String[] args) {
        // 定義發起人
        Originator originator = new Originator();
        // 定義備忘錄管理員
        Caretaker caretaker = new Caretaker();
        // 創建一個備忘錄
        caretaker.setMemento(originator.createMemento());
        // 恢復一個備忘錄
        originator.restoreMemento(caretaker.getMemento());
    }
}

4. clone 方式的備忘錄

我們可以通過複製的方式產生一個對象的內部狀態,這是一個很好的辦法,發起人角色只要實現 Cloneable 就成,比較簡單:

public class Originator implements Cloneable {
    // 內部狀態
    private String state;

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }

    private Originator backup;

    // 創建一個備忘錄
    public void createMemento() {
        this.backup = this.clone();
    }
    // 恢復一個備忘錄
    public void restoreMemento() {
        this.setState(this.backup.getState());
    }
    // 克隆當前對象
    @Override
    protected Originator clone() {
        try {
            return (Originator) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }
}

備忘錄管理員:

public class Caretaker {
    // 發起人對象
    private Originator originator;

    public Originator getOriginator() {
        return originator;
    }

    public void setOriginator(Originator originator) {
        this.originator = originator;
    }
}

場景類:

public class Client {
    public static void main(String[] args) {
        // 定義發起人
        Originator originator = new Originator();
        // 創建初始狀態
        originator.setState("初始狀態");
        System.out.println("初始狀態:" + originator.getState());
        // 創建備份
        originator.createMemento();
        // 修改狀態
        originator.setState("修改後的狀態");
        System.out.println("修改後的狀態:" + originator.getState());
        // 恢復狀態
        originator.restoreMemento();
        System.out.println("恢復後的狀態:" + originator.getState());
    }
}

運行結果是我們所希望的,程序精簡了很多,而且高層模塊的依賴也減少了,這正是我們期望的效果。

但是我們來考慮一下原型模式深拷貝和淺拷貝的問題,在複雜的場景下它會讓我們的程序邏輯異常混亂,出現錯誤也很難跟蹤。因此 Clone 方式的備忘錄模式適用於較簡單的場景。

5. 多備份的備忘錄

我們每天使用的 Windows 是可以擁有多個備份時間點的,系統出現問題,我們可以自由選擇需要恢復的還原點。

我們上面的備忘錄模式尚且不具有這個功能,只能有一個備份,想要有多個備份也比較簡單,我們在備份的時候做一個標記,簡單一點可以使用一個字符串。

我們只要把通用代碼中的 Caretaker 管理員稍做修改就可以了:

public class Caretaker {
    // 容納備忘錄的容器
    private Map<String, Memento> mementoMap = new HashMap<>();

    public Memento getMemento(String keys) {
        return mementoMap.get(keys);
    }

    public void setMemento(String key, Memento memento) {
        this.mementoMap.put(key, memento);
    }
}

對場景類做部分修改:

public class Client {
    public static void main(String[] args) {
        // 定義發起人
        Originator originator = new Originator();
        // 定義備忘錄管理員
        Caretaker caretaker = new Caretaker();
        // 創建兩個備忘錄
        caretaker.setMemento("001", originator.createMemento());
        caretaker.setMemento("002", originator.createMemento());
        // 恢復一個指定的備忘錄
        originator.restoreMemento(caretaker.getMemento("002"));
    }
}

6. 更好的封裝

在系統管理上,一個備份的數據是完全、絕對不能修改的,它保證數據的潔淨,避免數據污染而使備份失去意義。

在我們的程序中也有着同樣的問題,備份是不能被褚篡改的,那麼也就是需要縮小備忘錄的訪問權限,保證只有發起人可讀就可以了。

這個很簡單,直接使用內置類就可以了:

public class Originator {
    private String state;

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }
    // 創建一個備忘錄
    public IMemento createMemento() {
        return new Memento(this.state);
    }
    // 恢復一個備忘錄
    public void restoreMemento(IMemento memento) {
        this.setState(((Memento)memento).getState());
    }

    private class Memento implements IMemento {
        private String state;
        private Memento(String state) {
            this.state = state;
        }

        public String getState() {
            return state;
        }

        public void setState(String state) {
            this.state = state;
        }
    }
}

這裏使用了一個 IMemento 接口,這個接口實際上是一個空接口:

public interface IMemento {
}

這個空接口的作用是用作公共的訪問權限。

下面看一下備忘錄管理者的變化:

public class Caretaker {
    // 備忘錄對象
    private IMemento memento;

    public IMemento getMemento() {
        return memento;
    }

    public void setMemento(IMemento memento) {
        this.memento = memento;
    }
}

上面這段示例全部通過接口訪問,如果我們想訪問它的屬性貌似是無法訪問到了。

但是安全是相對的,沒有絕對的安全,我們可以使用 refelect 反射修改 Memento 的數據。

在這裏我們使用了一個新的設計方法:雙接口設計,我們的一個類可以實現多個接口,在系統設計時,如果考慮對象的安全問題,則可以提供兩個接口,一個是業務的正常接口,實現必要的業務邏輯,叫做寬接口;另外一個接口是一個空接口,什麼方法都沒有,其目的是提供給子系統外的模塊訪問,比如容器對象,這個叫做窄接口,由於窄接口中沒有提供任何操縱數據的方法,因此相對來說比較安全。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章