对备忘录模式的理解
一、场景
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) {
// 相当于还没有做任何操作,用户就执行了Undo
return 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) {
// 相当于还没有做任何操作,用户就执行了Undo
return 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; } }