Google Prettify

2015-05-06

Design Pattern 介紹:Memento

Memento design pattern的class diagram。圖片取自Wiki

Memento 這個pattern的使用範圍很明確,就是當你有rollback、undo這類需求的時候的時候,都可以使用。我們透個一個情境來介紹一下memento這個pattern 。

Ok,情境是這樣的…

你的需求是一個交易(Transaction)。使用者買東西時,會從帳號(Account)裡扣錢,每次扣1000元。我今天有一段很無聊(?)的程式(demoMemento),扣了2次錢之後,使用者硬是要rollback回去,當交易沒發生過。所以你必需將帳號的狀態設會原本扣2次錢之前的狀態。`

Memento design pattern情境的 class diagram

以下是RunDp.demoMemento()的程式。這一段是主程式。

package taichitech.jblog.dp;

public class RunDp {
    Logger logger = LoggerFactory.getLogger(getClass());

    public RunDp() {

    public static void main(String[] args) {
        RunDp rd = new RunDp();
        rd.demoMemento();
    }

    public void demoMemento() {
        AccountDao accountDao = new AccountDao();
        String accountId = "99004-PETER-22-114";
        int amount = -10000;
        //Get account from Dao and deduct amount x 2. 
        Account account = accountDao.findAccount(accountId);
        //Before deduct, we create a memento.
        account.createMemnto();
        account.setBalance(account.getBalance() + amount);
        logger.debug("Account info-1:{}", account);
        account.createMemnto();
        account.setBalance(account.getBalance() + amount);
        logger.debug("Account info-2:{}", account);
        account.restoreMemnto(2);
        logger.debug("Rollbacked Account info:{}", account);
    }
}

main那些什麼的都不是我們要講的重點;demoMemento()才是主程式的重點。

程式一開始產生了一個AccountDao,超顧名思義的,就是可以透過一個accountId取得一個Account物件。

Account物件可以做3件事:1. 建立memento(createMemnto()),2. 扣款(Account.setBalane()),3. 回復狀態(restoreMemnto())。

RunDp取得Account後,首先建立了一個memento、然後扣款(-10000)、印出訊息;這樣的動作一共做了2次。然後使用者閒東西太貴又想取消交易,於是我們只好將狀態回到2次交易前的樣子(restoreMemnto(2))。

接下來我們看一下Account,這部份才是我們要提的memento重點:

package taichitech.jblog.dp.vo;

public class Account {

    @Override
    public String toString() {
        return "Account [accountId=" + accountId + ", balance=" + balance + ", snapshot=" + snapshots + "]";
    }

    private String accountId;

    private int balance;

//...Other getter and setter of properties.

    private ArrayList<Memento> snapshots = new ArrayList<>();

    private class Memento {
        int balance;

        public int getBalance() {
            return balance;
        }

        public void setBalance(int balance) {
            this.balance = balance;
        }
        @Override
        public String toString() {
            return "Memento [balance=" + balance + "]";
        }
    }

    public void createMemnto() {
        Memento memento = new Memento();
        memento.setBalance(this.balance);
        snapshots.add(memento);
    }

    /** @param undoTimes Times of undo. */
    public void restoreMemnto(int undoTimes) {
        undoTimes = (snapshots.size() < undoTimes ? snapshots.size() : undoTimes);
        for (int i = 0; i < undoTimes; i++) {
            int lastIndex = snapshots.size() - 1;
            this.balance = snapshots.get(lastIndex).getBalance();
            snapshots.remove(lastIndex);
        }
    }
`}

Account除了記錄餘額外,裡面還有一個snapshots的property。 他是一個Mement class 的集合(ArrayList),這是用來記錄每次的狀態,每createMemnto()一次,就會產生一個Memento物件加到集合裡。

Memento class是一個innter class。使用innter class主要是基於權限的關係。RunDp或是其他的Programmer、維運人員,可以完全不用知道裡面的狀況。 這樣除了可以避免不小心改到不相關的程式碼外,也可以避免混淆視聽。 當然,你也可以把Memento開放出來,讓RunDp、或是其他人員進行其他的操作。 這裡指的其他 不應該包含會影響到Account原本狀態的操作。 有關於Account狀態的一些操作,應該將權限綁成全部由Account來進行操作。

另外一個restoreMemnto() 是將Account的狀態回復。參數是回復次數,可以回復前undoTimes次。裡面的迴圈,會依次取得最後一次Memento的物件,然後將資料回設給Account。回設後會將該物件由snapshots中拿掉。 當然,以design pattern來說,裡面的那些個回復、怎麼的回復,也不是重點;重點是memento design pattern 通常會有個method,以供回復狀態

在實作上,你可以直接指定ArrayList.get(index),取得特定的Memento,然後移除該index後的所有元素,這樣看似會比較有效率一些;而要不要移除元素,這也完全看需求,你也可以保留做為歷史追蹤。

這裡是執行結果:

868 [main] DEBUG t.jblog.dp.RunDp[37]-Account info-1:Account [accountId=99004-PETER-22-114, balance=690000, snapshot=[Memento [balance=700000]]]
869 [main] DEBUG t.jblog.dp.RunDp[40]-Account info-2:Account [accountId=99004-PETER-22-114, balance=680000, snapshot=[Memento [balance=700000], Memento [balance=690000]]]
869 [main] DEBUG t.jblog.dp.RunDp[42]-Rollbacked Account info:Account [accountId=99004-PETER-22-114, balance=700000, snapshot=[]]

請注意,這裡的回復是指「回到原本的狀態」不是「將錢還回去」。這2種描述在實作上有很大的差別。 第一種是像demo程式,直接將狀態「設」回去。第2種則是你必需要記錄每次所扣的錢,然後一次一次「加」回去。也就是第2種描述,你必記錄的不見得是變更前的「狀態」,而是每次變更的「異動」,例如-10000。

限制

  1. 一個最明顯的問題,當然是記憶體的問題。每mementdo一次,就是一個記憶體;可以備份多少次、要備份那些狀態,當然要量「機」而為。
  2. 如果你將Memento class 開放給Account以外的類別使用,需要注意開放的範圍;不可以讓開放的操作影響到Account的狀態。 像Memento.getBalance()Memento.seBalance()就應該只能由Account來呼叫,不能由RunDp呼叫。

結語

在原著上,Memento 這個pattern有3個class:CaretakerOriginatorMemento;對應到我們的demo code,分別是RunDpAccountAccount$Memento(這是一個innter class)。 Caretaker操作Originator的方法,以決定什麼時候需要memento。 Memento儲存Originator的狀態。 Caretaker可以操作Memento,不過不應該操作Orignator狀態的相關資訊;Originator狀態的變更,只應交由Originator來操作。 Memento在儲存Originator狀態時,可以有不同的儲存方式與應用方式,例如備份狀態,或是備份差異,當然,備份時也需要考慮到resource是否足夠或是resource 的釋放。

個人覺得使用機會是比factory method、facade 或是其他的pattern要來得小的多。 儘管如此,當有用上的時候,他還是很有用。

沒有留言 :

您或許對這些有興趣

Related Posts with Thumbnails

最後

謝謝您的閱讀,希望您可以有豐富的收獲。