对备忘录模式的理解
- 一、场景
- 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;} }