Memento 這個pattern的使用範圍很明確,就是當你有rollback、undo這類需求的時候的時候,都可以使用。我們透個一個情境來介紹一下memento這個pattern 。
你的需求是一個交易(Transaction)。使用者買東西時,會從帳號(Account)裡扣錢,每次扣1000元。我今天有一段很無聊(?)的程式(demoMemento),扣了2次錢之後,使用者硬是要rollback回去,當交易沒發生過。所以你必需將帳號的狀態設會原本扣2次錢之前的狀態。`
以下是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。
限制
- 一個最明顯的問題,當然是記憶體的問題。每mementdo一次,就是一個記憶體;可以備份多少次、要備份那些狀態,當然要量「機」而為。
- 如果你將
Mementoclass 開放給Account以外的類別使用,需要注意開放的範圍;不可以讓開放的操作影響到Account的狀態。 像Memento.getBalance()與Memento.seBalance()就應該只能由Account來呼叫,不能由RunDp呼叫。
結語
在原著上,Memento 這個pattern有3個class:Caretaker、Originator、Memento;對應到我們的demo code,分別是RunDp、Account、Account$Memento(這是一個innter class)。 Caretaker操作Originator的方法,以決定什麼時候需要memento。 Memento儲存Originator的狀態。 Caretaker可以操作Memento,不過不應該操作Orignator狀態的相關資訊;Originator狀態的變更,只應交由Originator來操作。 Memento在儲存Originator狀態時,可以有不同的儲存方式與應用方式,例如備份狀態,或是備份差異,當然,備份時也需要考慮到resource是否足夠或是resource 的釋放。
個人覺得使用機會是比factory method、facade 或是其他的pattern要來得小的多。 儘管如此,當有用上的時候,他還是很有用。
沒有留言 :
張貼留言