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

1. 前文彙總

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

2. 從版本控制開始

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

版本管理工具是讓咱們在代碼出問題的時候,能夠方便的獲取到以前的版本進行版本回退,尤爲是在項目發佈投運的時候,當出現問題的時候直接獲取上一個版本進行回滾操做。設計模式

在這個操做中間,最重要的就是保存以前的狀態,那麼如何保存以前的狀態?安全

操做很簡單,咱們能夠定義一箇中間變量,保留這個原始狀態。ide

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

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

程序運行正確,輸出結果也是咱們指望的,可是結果正確並不表示程序是合適的。this

在場景類 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 的數據。

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

相關文章
相關標籤/搜索