對備忘錄模式的理解
- 一、場景
- 1、題目【[來源](https://kamacoder.com/problempage.php?pid=1095)】
- 1.1 題目描述
- 1.2 輸入描述
- 1.3 輸出描述
- 1.4 輸入示例
- 1.5 輸出示例
- 2、理解需求
- 二、不采用備忘錄設計模式
- 1、代碼
- 2、問題
- 3、錯誤的備忘錄模式
- 三、采用備忘錄設計模式
- 1、代碼
- 1.1 Originator(原發器)
- 1.2 Memento(備忘錄)
- 1.3 Caretaker(負責人)
- 1.4 客戶端
- 2、思考
一、場景
1、題目【來源】
1.1 題目描述
小明正在設計一個簡單的計數器應用,支持增加(Increment)和減少(Decrement)操作,以及撤銷(Undo)和重做(Redo)操作,請你使用備忘錄模式幫他實現。
1.2 輸入描述
輸入包含若干行,每行包含一個字符串,表示計數器應用的操作,操作包括 “Increment”、“Decrement”、“Undo” 和 “Redo”。
1.3 輸出描述
對于每個 “Increment” 和 “Decrement” 操作,輸出當前計數器的值,計數器數值從0開始 對于每個 “Undo” 操作,輸出撤銷后的計數器值。 對于每個 “Redo” 操作,輸出重做后的計數器值。
1.4 輸入示例
Increment
Increment
Decrement
Undo
Redo
Increment
1.5 輸出示例
1
2
1
2
1
2
2、理解需求
-
增加(Increment)和減少(Decrement)操作比較好理解,不贅述了。
-
重點理解下:撤銷(Undo)和重做(Redo)操作。
?
?
-
一般編輯器,都支持Undo和Redo操作。
-
Undo操作:因為操作導致值發生變化,例如,0變成1。我們需要記下變化的值,這樣才方便用戶回退。
- 很明顯,應該用棧來記錄。例如:0 -> 1 -> 2 -> 3。 當前處于3,接下來Undo,應該從3變成2。也就是把3從棧中彈出。
-
Redo操作:依然基于“0 -> 1 -> 2 -> 3”進行說明,當前處于3,用戶Undo后,3變成2,接下來,用戶Redo了,也就是希望2又變回3。
- 也就是,我們需要記錄Undo棧中彈出來的值。很顯然,也是一個棧。
-
二、不采用備忘錄設計模式
1、代碼
public class Main {public static void main(String[] args) {Scanner scanner = new Scanner(System.in);int res = 0;// 棧Deque<Integer> undoStack = new ArrayDeque<>();undoStack.push(res);Deque<Integer> redoStack = new ArrayDeque<>();while (scanner.hasNextLine()) {String command = scanner.nextLine();res = runCommand(command, res, undoStack, redoStack);System.out.println(res);}}private static Integer runCommand(String command, Integer res, Deque<Integer> undoStack, Deque<Integer> redoStack) {if ("Increment".equals(command)) {res += 1;undoStack.push(res);return res;} else if ("Decrement".equals(command)) {res -= 1;undoStack.push(res);return res;} else if ("Undo".equals(command)) {if (undoStack.size() == 1) {// 相當于還沒有做任何操作,用戶就執行了Undoreturn undoStack.peek();} else if (undoStack.size() > 1) {Integer value = undoStack.pop();redoStack.push(value);return undoStack.peek();}} else if ("Redo".equals(command)) {if (!redoStack.isEmpty()) {Integer value = redoStack.pop();undoStack.push(value);return value;}}return res;}
}
2、問題
-
Increment等操作是客戶端(main方法)的命令,客戶端不應該看到undoStack、redoStack等數據。
-
上面的寫法是典型的面向過程開發,我們需要使用面向對象開發。
?
?
-
-
很顯然,我們需要設計一個Calculator。
-
public class Calculator {private int value;public Calculator() {this.value = 0;}public Integer runCommand(String command) {return null;} }public class Main {public static void main(String[] args) {Scanner scanner = new Scanner(System.in);Calculator calculator = new Calculator();while (scanner.hasNextLine()) {String command = scanner.nextLine();Integer res = calculator.runCommand(command);System.out.println(res);}} }
-
-
為了實現Undo、Redo,這個類的對象有一個特點,需要保存和恢復對象之前的狀態。
- 備忘錄模式是一種行為設計模式, 允許在不暴露對象實現細節的情況下保存和恢復對象之前的狀態。[先有場景,后有設計模式]
3、錯誤的備忘錄模式
public class Calculator {private int value;private Deque<Integer> undoStack;private Deque<Integer> redoStack;public Calculator() {this.value = 0;this.undoStack = new ArrayDeque<>();undoStack.push(value);this.redoStack = new ArrayDeque<>();}public Integer runCommand(String command) {if ("Increment".equals(command)) {value += 1;undoStack.push(value);return value;} else if ("Decrement".equals(command)) {value -= 1;undoStack.push(value);return value;} else if ("Undo".equals(command)) {if (undoStack.size() == 1) {// 相當于還沒有做任何操作,用戶就執行了Undoreturn undoStack.peek();} else if (undoStack.size() > 1) {Integer v = undoStack.pop();redoStack.push(v);return undoStack.peek();}} else if ("Redo".equals(command)) {if (!redoStack.isEmpty()) {Integer v = redoStack.pop();undoStack.push(v);return v;}}return value;}
}
-
Calculator這個類是違背單一職責的,按照備忘錄模式的經典設計,應該具有3個角色:
- Originator(原發器):狀態持有者,并且可以請求保存狀態和恢復狀態。
- Memento(備忘錄):負責保存狀態和恢復狀態。
- Caretaker(負責人):負責管理備忘錄。
三、采用備忘錄設計模式
1、代碼
1.1 Originator(原發器)
public class Counter {private int value;public Counter() {this.value = 0;}public int getValue() {return value;}public void increment() {this.value++;}public void decrement() {this.value--;}public Memento createMemento() {return new Memento(this.value);}public void restoreMemento(Memento memento) {this.value = memento.getValue();}
}
1.2 Memento(備忘錄)
public class Memento {private int value;public Memento(int value) {this.value = value;}public int getValue() {return value;}
}
1.3 Caretaker(負責人)
public class Calculator {private Counter counter;private Deque<Memento> undoStack;private Deque<Memento> redoStack;public Calculator() {counter = new Counter();undoStack = new ArrayDeque<>();redoStack = new ArrayDeque<>();}public Integer runCommand(String command) {if (command.equals("Increment")) {counter.increment();undoStack.push(counter.createMemento());redoStack.clear(); // redoStack是專門用來記錄undoStack彈出的狀態的,undoStack放入新狀態后,redoStack里面的狀態就無效了} else if (command.equals("Decrement")) {counter.decrement();undoStack.push(counter.createMemento());redoStack.clear();} else if (command.equals("Undo")) {if (!undoStack.isEmpty()) {Memento memento = undoStack.pop();redoStack.push(memento);counter.restoreMemento(undoStack.peek());}} else if (command.equals("Redo")) {if (!redoStack.isEmpty()) {Memento memento = redoStack.pop();counter.restoreMemento(memento);undoStack.push(memento);}} else {throw new RuntimeException("Unknown command");}return counter.getValue();}
}
1.4 客戶端
public class Main {public static void main(String[] args) {Scanner scanner = new Scanner(System.in);Calculator calculator = new Calculator();while (scanner.hasNextLine()) {String command = scanner.nextLine();Integer res = calculator.runCommand(command);System.out.println(res);}}
}
2、思考
-
相比“3、錯誤的備忘錄模式”,每個類的職責更單一一些。但,Memento好麻煩啊。相當于把Counter的字段復制了一遍。以后Counter加一個字段,Memento就要補一個字段。太麻煩了。
-
一種不錯的解決辦法是:序列化。
-
public class Counter {private int value;public Counter() {}public void setValue(int value) {this.value = value;}public int getValue() {return value;}public void increment() {this.value++;}public void decrement() {this.value--;}public void restoreMemento(Memento memento) {String backup = memento.getBackup();Counter tmpCounter = JSON.parseObject(backup, Counter.class);this.value = tmpCounter.value;}public Memento createMemento() {String backup = JSON.toJSONString(this);return new Memento(backup);} }public class Memento {private String backup;public Memento(String backup) {this.backup = backup;}public String getBackup() {return this.backup;} }