备忘录模式
一、介绍
-
备忘录模式是一种行为设计模式,它允许在不破坏对象封装性的前提下,捕获并外部化一个对象的内部状态,以便日后可以恢复该对象到之前保存的状态。
-
假设你在玩一个电子游戏,游戏中有一个保存进度的功能。当你保存游戏时,实际上是将游戏的当前状态(比如关卡、分数、生命值等)存储到一个对象(备忘录)中。当你想恢复到保存的进度时,游戏只需从备忘录对象中读取相关数据,从而将自身恢复到之前的状态。
-
有个3个角色
发起者(Originator):需要保存和恢复状态的对象。
备忘录(Memento):用于存储发起者对象的内部状态。
管理者(Caretaker):负责存储备忘录对象,但不能修改备忘录的内容。
二、绘图项目中使用备忘录
需求:开发一个绘图应用程序,用户可以在画布上绘制各种形状和线条。我们需要为程序添加"撤销"和"重做"功能,以便用户可以撤销或重做之前的操作。
实现步骤:
- 定义备忘录(Memento)类,用于存储画布当前的状态(所有已绘制的形状和线条)。
- 定义发起人(Originator)类,表示画布对象,负责创建备忘录对象并从中还原状态。
- 定义管理者(Caretaker)类,负责存储多个备忘录对象,以便在需要时恢复到特定状态。
- 在发起人(Originator)类中,实现创建备忘录对象和从备忘录对象还原状态的方法。
- 在管理者(Caretaker)类中,实现存储和获取备忘录对象的方法。
- 在应用程序的主逻辑中,调用发起人和管理者的相关方法,实现"撤销"和"重做"功能。
java
// 备忘录类,存储画布当前状态
class Memento {
private final List<Shape> shapes; // 存储所有已绘制的形状
public Memento(List<Shape> shapes) {
this.shapes = new ArrayList<>(shapes);
}
public List<Shape> getShapes() {
return new ArrayList<>(shapes);
}
}
// 发起人类,表示画布对象
class Canvas {
private final List<Shape> shapes = new ArrayList<>(); // 存储已绘制的形状
public void addShape(Shape shape) {
shapes.add(shape);
}
public void removeShape(Shape shape) {
shapes.remove(shape);
}
public Memento createMemento() {
return new Memento(shapes);
}
public void restoreFromMemento(Memento memento) {
shapes.clear();
shapes.addAll(memento.getShapes());
}
}
// 管理者类,负责存储多个备忘录对象
class CareTaker {
private final Deque<Memento> mementos = new LinkedList<>();
private final int maxSize = 10; // 最多存储10个备忘录对象
public void addMemento(Memento memento) {
mementos.offerLast(memento);
if (mementos.size() > maxSize) {
mementos.pollFirst(); // 移除最早的备忘录对象
}
}
public Memento getUndo() {
return mementos.pollLast();
}
public Memento getRedo() {
return mementos.peekLast();
}
}
// 形状类,表示在画布上绘制的形状
class Shape {
private final String type;
private final int x, y;
public Shape(String type, int x, int y) {
this.type = type;
this.x = x;
this.y = y;
}
// 省略 getter 方法...
}
public class Main {
public static void main(String[] args) {
Canvas canvas = new Canvas();
CareTaker careTaker = new CareTaker();
// 绘制几个形状
canvas.addShape(new Shape("Rectangle", 10, 10));
canvas.addShape(new Shape("Circle", 20, 20));
careTaker.addMemento(canvas.createMemento()); // 保存当前状态
canvas.addShape(new Shape("Triangle", 30, 30));
careTaker.addMemento(canvas.createMemento()); // 保存当前状态
// 撤销一步
Memento undoMemento = careTaker.getUndo();
if (undoMemento != null) {
canvas.restoreFromMemento(undoMemento);
}
// 重做一步
Memento redoMemento = careTaker.getRedo();
if (redoMemento != null) {
canvas.restoreFromMemento(redoMemento);
careTaker.addMemento(redoMemento); // 添加重做的状态
}
// 输出当前画布上的形状
for (Shape shape : canvas.getShapes()) {
System.out.println(shape);
}
}
}
我们定义了Memento
类来存储画布的当前状态,Canvas
类作为发起人管理所有已绘制的形状,并提供创建和恢复备忘录的方法。CareTaker
类负责存储多个备忘录对象,以便可以撤销和重做操作。Main
类展示了如何使用这些类来实现"撤销"和"重做"功能。
三、HashMap中使用
在Java中,java.util.HashMap
的实现中使用了备忘录模式。当HashMap
需要进行rehash操作(例如在put操作导致容量超过阈值时)时,它会创建一个新的更大的底层数组,并将原有数组中的元素迁移到新数组中。为了避免重复计算元素的哈希值,HashMap
会在迁移元素时,将元素的哈希值作为一个"备忘录"保存在Node
对象中。
下面是HashMap
中Node
内部类的源码:
java
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
可以看到,Node
对象存储了键值对以及键的哈希值。当HashMap
需要进行rehash操作时,它会遍历原有数组,根据元素的哈希值计算在新数组中的位置,并将元素复制到新位置,而无需重新计算哈希值。这样做可以提高rehash
操作的效率。
四、备忘录模式的优缺点及使用经验
优点:
- 保护对象的封装性: 备忘录模式将对象的状态信息封装在备忘录对象中,而不是直接暴露对象的内部状态,从而保护了对象的封装性。
- 简化发起人类: 由于状态信息被存储在备忘录对象中,因此发起人类可以专注于实现自身的功能逻辑,而无需处理状态存储和恢复的复杂逻辑。
- 支持撤销/重做操作: 备忘录模式天生支持撤销和重做操作,只需存储多个备忘录对象即可。
缺点:
- 占用额外内存空间: 每次创建备忘录对象时,都需要消耗一定的内存空间来存储状态信息,如果频繁创建备忘录对象,可能会导致内存占用过高。
- 存在效率问题: 如果对象的状态信息过于庞大,创建和存储备忘录对象可能会影响系统的效率。
使用经验:
- 不要滥用备忘录模式: 备忘录模式虽然提供了方便的状态存储和恢复功能,但也会带来一定的性能和内存开销。因此,在非必要场景下,不建议使用备忘录模式。
- 评估状态信息的大小: 在使用备忘录模式前,应该评估对象状态信息的大小,如果状态信息过于庞大,可能会导致性能和内存问题。
- 考虑使用其他模式: 在某些场景下,备忘录模式可能不是最佳选择。例如,如果只需要存储和恢复少量状态信息,可以考虑使用观察者模式或状态模式等其他设计模式。
- 合理管理备忘录对象: 为了避免占用过多内存,可以限制存储备忘录对象的数量,或者在适当时机清理无用的备忘录对象。
- 封装备忘录对象: 为了保护对象的封装性,备忘录对象应该只能由发起人类访问和修改,而不能被其他类直接访问。
备忘录模式在需要存储和恢复对象状态的场景中非常有用,但也需要合理使用,避免滥用导致性能和内存问题。