备忘录模式允许在不破坏封装性下捕获并在外部保存对象状态,支持状态恢复,常用于撤销、历史记录等功能。例如在线文档编辑器的撤销操作,编辑器作为原发起人记录状态并提供保存与恢复方法,历史记录或撤销为管理者,保存备忘录对象,每个状态点即为备忘录,此模式为状态管理提供了灵活强大的机制。
定义
备忘录模式是一种行为型设计模式,它允许在不违反封装性的前提下捕获一个对象的内部状态,并在对象之外保存这个状态,以后可以恢复对象到这个状态,它常用于实现撤销操作、历史记录、快照等功能。
举一个业务中的例子:假设你正在使用一个在线文档编辑器,在这个编辑器中,你可以编写文档、添加格式、插入图片等,同时,这个编辑器还提供了一个非常有用的功能:历史版本或撤销操作,当你编写文档时,不小心删除了一段重要的文字,这时你不需要重新输入,而是可以简单地点击“撤销”按钮,刚刚删除的文字就会重新出现,这就是备忘录模式的一个典型应用。
在这个例子中:
- 文档编辑器:这是原发起人(Originator),它记录了当前文档的状态,并提供了创建备忘录(保存状态)和恢复状态的方法。
- 历史记录或撤销:这相当于备忘录管理者(Caretaker),它负责保存和管理备忘录对象,每次你修改文档时,编辑器都会在背后创建一个备忘录对象并交给管理者保存。
- 状态点:这就是备忘录(Memento)对象,它保存了文档在某个特定时间点的状态,这些对象被管理者保存起来,以便在需要时恢复。
通过这种方式,备忘录模式允许在不破坏对象封装性的情况下保存和恢复对象的状态,从而提供了一种灵活且强大的状态管理机制。
代码案例
反例
以下是一个未使用备忘录模式的反例代码。在未使用备忘录模式的情况下,如果需要实现撤销功能,会直接在业务对象中保存历史状态,或者在client代码中管理状态历史,这样会破坏对象的封装性,增加业务对象的复杂性,并且使得客户端代码与业务逻辑紧密耦合,如下代码:
// TextDocument.java
public class TextDocument {
private String content;
private final Stack<String> history; // 直接在文档类中管理历史记录
public TextDocument() {
this.content = "";
this.history = new Stack<>();
// 初始状态也加入历史记录,方便演示
history.push(content);
}
public void setContent(String content) {
// 在修改内容之前,先将当前内容保存到历史记录中
this.history.push(this.content);
this.content = content;
}
public String getContent() {
return content;
}
// 撤销功能实现,直接从历史记录中取出上一个状态
public void undo() {
if (!history.isEmpty()) {
// 弹出当前状态,因为要回退到前一个状态
history.pop();
// 如果历史记录为空,说明已经回退到最初状态
if (history.isEmpty()) {
this.content = "";
} else {
// 否则,将前一个状态设置为当前内容
this.content = history.peek();
}
}
}
}
// Client.java 客户端代码
public class Client {
public static void main(String[] args) {
TextDocument document = new TextDocument();
System.out.println("初始内容: " + document.getContent());
document.setContent("第一次编辑");
System.out.println("编辑后内容: " + document.getContent());
document.undo();
System.out.println("撤销后内容: " + document.getContent());
// 尝试再次撤销,将会回到初始状态
document.undo();
System.out.println("再次撤销后内容: " + document.getContent());
// 再次尝试撤销,内容应保持不变
document.undo();
System.out.println("无法再撤销,内容保持为: " + document.getContent());
}
}
运行结果,如下:
初始内容:
编辑后内容: 第一次编辑
撤销后内容:
再次撤销后内容:
无法再撤销,内容保持为:
在这个例子中,TextDocument
类直接管理了自己的历史记录,这破坏了封装性,因为历史记录管理并不是文档编辑的核心职责,撤销操作的实现存在逻辑错误,当调用undo
方法时,应该回退到前一个状态,但代码中的实现会导致内容被清空,而不是回退到正确的状态。
正例
正例以下是一个使用备忘录模式的正例代码,当使用备忘录模式时,创建一个备忘录类来存储原发起人的内部状态,并由原发起人自己创建和恢复备忘录,此外,还需要一个管理者类来负责保存备忘录对象,但不需要了解备忘录的具体内容,这样,原发起人可以在不破坏封装性的情况下保存和恢复其内部状态,如下代码:
// Memento.java - 备忘录类,用于存储TextDocument的内部状态
public class Memento {
private final String content;
public Memento(String content) {
this.content = content;
}
public String getContent() {
return content;
}
}
// TextDocument.java - 原发起人,负责创建备忘录和管理状态
public class TextDocument {
private String content;
public TextDocument() {
this.content = "";
}
public void setContent(String content) {
this.content = content;
}
public String getContent() {
return content;
}
// 创建备忘录,保存当前状态
public Memento saveToMemento() {
return new Memento(content);
}
// 恢复状态,从备忘录中恢复
public void restoreFromMemento(Memento memento) {
this.content = memento.getContent();
}
}
// Caretaker.java - 管理者类,负责保存备忘录对象,但不了解其内容
import java.util.Stack;
public class Caretaker {
private final Stack<Memento> savedStates = new Stack<>();
public void addMemento(Memento memento) {
savedStates.push(memento);
}
public Memento getMemento() {
return savedStates.pop();
}
// 检查是否有保存的状态可供恢复
public boolean hasSavedStates() {
return !savedStates.isEmpty();
}
}
// Client.java - 客户端代码,演示如何使用备忘录模式
public class Client {
public static void main(String[] args) {
TextDocument document = new TextDocument();
Caretaker caretaker = new Caretaker();
System.out.println("初始内容: " + document.getContent());
document.setContent("第一次编辑");
caretaker.addMemento(document.saveToMemento());
System.out.println("编辑后内容: " + document.getContent());
document.setContent("第二次编辑");
System.out.println("再次编辑后内容: " + document.getContent());
// 使用备忘录恢复状态
if (caretaker.hasSavedStates()) {
document.restoreFromMemento(caretaker.getMemento());
System.out.println("撤销后内容: " + document.getContent());
}
// 尝试再次撤销,但没有更多的保存状态了
if (caretaker.hasSavedStates()) {
document.restoreFromMemento(caretaker.getMemento());
System.out.println("再次撤销后内容: " + document.getContent());
} else {
System.out.println("没有更多的状态可以撤销了。");
}
}
}
运行结果:
初始内容:
编辑后内容: 第一次编辑
再次编辑后内容: 第二次编辑
撤销后内容: 第一次编辑
没有更多的状态可以撤销了。
代码解释:
- Memento类是一个简单的Java Bean,用于存储TextDocument的状态。
- TextDocument类有设置和获取内容的方法,以及创建备忘录和从备忘录中恢复状态的方法。
- Caretaker类负责管理备忘录对象,它使用一个栈来存储备忘录,以便可以依次撤销多个操作。
这个实现保持了TextDocument类的封装性,因为管理者类Caretaker不需要了解TextDocument的内部实现细节,同时,client代码可以轻松地保存和恢复文档的状态,而不需要直接操作文档的内部状态。
核心总结
备忘录模式允许在不破坏封装性的前提下捕获对象的内部状态,并在以后将对象恢复到该状态,其优点主要在于可以提供一种状态恢复机制,实现撤销、历史记录等功能,增强系统的灵活性和可用性,同时,由于备忘录只暴露给原发起人,这保护了发起人的封装性,防止外部对象访问其内部状态。备忘录模式也存在缺点,它可能会消耗较多的内存资源,因为需要存储多个备忘录对象,此外,管理备忘录的责任需要明确,否则可能导致状态管理的混乱。
在使用备忘录模式时,要慎重考虑是否需要提供状态恢复功能,因为不是所有系统都需要这种功能,要注意管理备忘录的生命周期,避免内存泄漏。