Memento 這個pattern的使用範圍很明確,就是當你有rollback、undo這類需求的時候的時候,都可以使用。我們透個一個情境來介紹一下memento這個pattern 。
你的需求是一個交易(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。
限制
- 一個最明顯的問題,當然是記憶體的問題。每mementdo一次,就是一個記憶體;可以備份多少次、要備份那些狀態,當然要量「機」而為。
- 如果你將
Memento
class 開放給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要來得小的多。 儘管如此,當有用上的時候,他還是很有用。
沒有留言 :
張貼留言