本篇讲解Java设计模式中的备忘录模式,分为定义、模式应用前案例、结构、模式应用后案例、适用场景、模式可能存在的困惑和本质探讨7个部分。
定义
备忘录模式是在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可以将该对象恢复到保存的状态。
在新的分类方式中,备忘录模式被划分至类属性相关需求类别中,其应对的是类的状态属性需要恢复的要求。
模式应用前案例
文本编辑器是一个备忘录模式的典型应用场景。接下来,先来看一下未使用备忘录模式之前的代码实现。
public class TextEditor {//编辑器类-直接实现保存和恢复操作
private String content;
private String previousContent;
public void write(String text) {
if(this.content == null ) {
this.content = "";
}
this.content += text;
}
// 保存当前内容为上一个版本的状态
public void save() {
this.previousContent = this.content;
}
// 恢复到上一个版本的状态
public void undo(){
if(this.previousContent != null){
this.content = this.previousContent;
}
}
// 获取内容
public String getContent(){
return this.content;
}
}
public class Client {//调用方代码
public static void main(String[] ars){
TextEditor editor=new TextEditor();
editor.write("Hello, ");
System.out.println(editor.getContent());
editor.save();
editor.write("World!");
System.out.println(editor.getContent());
editor.undo();
System.out.println(editor.getContent());
}
}
在上述代码中,主要问题出现在TextEditor类中。为了实现恢复到上一步这个操作,在类中增加了previousContent属性。
如果这个功能是后来才需要增加的,则违背了OCP开闭原则。此外,如果后续要增加恢复上两步的操作,是否还要新增一个doublepreviousContent属性。显然,对于这种类状态(或属性)有变化且能够恢复的场景,应该有更好的解决方案。
结构
备忘录模式的示例实现代码如下。
public class Originator {
private String state;
public Memento createMemento() {
return new Memento(state);
}
public void setMemento(Memento memento) {
this.state = ((Memento) memento).getState();
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
}
public class Memento{
private final String state;
public Memento(String state) {
this.state = state;
}
public String getState() {
return state;
}
}
public class Caretaker {
private Memento memento;
public void setMemento(Memento memento) {
this.memento = memento;
}
public Memento getMemento() {
return memento;
}
}
public class Client {
public static void main(String[] args) {
// 创建Originator对象
Originator originator = new Originator();
// 设置初始状态
originator.setState("State 1");
System.out.println("Initial State: " + originator.getState());
// 创建Caretaker对象并保存备忘录
Caretaker caretaker = new Caretaker();
caretaker.setMemento(originator.createMemento());
// 改变Originator的状态
originator.setState("State 2");
System.out.println("State after change: " + originator.getState());
// 恢复到之前保存的状态
originator.setMemento(caretaker.getMemento());
System.out.println("State after restore: " + originator.getState());
}
}
从备忘录模式的结构和示例代码中,可以看到原有类Originator仅保留了与自身核心业务功能相关的属性,并将其需要恢复状态的属性state放在一个Memento类中保存。
Originator增加了两个比较简洁的方法,一个是创建Memento,一个是从Memento中恢复,所以setMemento方法使用restoreFromMemento会更加准确。
同时,增加了一个Caretaker类,它用于保存、恢复Memento。是恢复到上一个状态还是上两个状态都由Caretaker类专门负责。
不难发现,在备忘录模式下,各个类职责分工明确,核心类Originator专注于核心业务功能,Memento和Caretaker两个支撑类则用于实现状态的保存和恢复。
模式应用后案例
上面文本编辑器的案例,在应用备忘录模式之后的代码实现如下。
TextEditor类删掉了PreviousContent属性,职责更加单一。
public class TextEditor {// 编辑器类(Originator)- 负责创建备忘录和恢复到之前状态
private String content;
public void write(String text) {
if(this.content == null) {
this.content = "";
}
this.content += text;
}
// 创建当前内容对应的备份
public EditorMemento createMemento(){
return new EditorMemento(this.content);
}
// 从传入Mememtor对象中获取内容并进行还原
public void restoreFromMemento(EditorMemento memento){
this.content = memento.getContent();
}
public String getContent() {
return this.content;
}
}
增加EditorMemento和UndoManager两个类,分别实现TextEditor中Content属性的保存,以及EditorMemento的管理。
public class EditorMemento {// 备忘录类(Memento)- 存储文本编辑器的状态
private final String content;
public EditorMemento(String content) {
this.content = content;
}
public String getContent() {
return this.content;
}
}
public class UndoManager {// 管理者类(Caretaker)-负责管理保存和恢复操作
Stack<EditorMemento> emStack =new Stack<>();
public void save(EditorMemento memento){
this.emStack.push(memento);
}
public EditorMemento undo(){
if(!this.emStack.empty()){
return this.emStack.pop();
}
return null;
}
}
最后,调用方代码如下。
public class Client {//调用方代码
public static void main(String[] ars){
TextEditor editor = new TextEditor();
UndoManager undoManager=new UndoManager();
editor.write("Hello, ");
undoManager.save(editor.createMemento());
editor.write("World!");
//undoManager.save(editor.createMemento());
System.out.println(editor.getContent());
editor.restoreFromMemento(undoManager.undo());
System.out.println(editor.getContent());
}
}
适用场景
备忘录模式适用的场景非常明确,就是原有类在生命周期变化过程中,其属性的状态还可能需要恢复的场景。
模式可能存在的困惑
困惑1:为什么要有Caretaker类,为什么不能在Memento或Originator中实现保存和恢复功能,这样程序更加简洁?
如果在Originator中实现,又违背了SRP单一职责和OCP开闭原则;如果在Memento实现,这个类功能会变多,每次在Originator中创建Memento对象会占用更多内存,从这个角度就不合适。
困惑2:Memento类只是一个数据的封装类,为什么Originator的状态属性不能直接放在Caretaker中通过一个数据属性来实现?
实际上,许多人在考虑状态恢复的策略时,通常会优先想到这个方案。为了更好地进行说明,这里将代码实现罗列出来。
public class TextEditor {// 编辑器类(Originator)- 负责创建备忘录和恢复到之前状态
private String content;
public void write(String text) {
if(this.content == null) {
this.content = "";
}
this.content += text;
}
// 创建当前内容对应的备份
public void saveContent(){
UndoManager.save(this.content);
}
// 获取内容并进行还原
public void restoreFromContent(){
this.content = UndoManager.undo();
}
public String getContent() {
return this.content;
}
}
public class UndoManager {// 管理者类(Caretaker)-负责管理保存和恢复操作
private static final Stack<String> emStack =new Stack<>();
public static void save(String content){
emStack.push(content);
}
public static String undo(){
if(!emStack.empty()){
return emStack.pop();
}
return null;
}
}
public class Client {//调用方代码
public static void main(String[] ars){
TextEditor editor = new TextEditor();
editor.write("Hello, ");
editor.saveContent();
editor.write("World!");
System.out.println(editor.getContent());
editor.restoreFromContent();
System.out.println(editor.getContent());
}
}
这种方式下,似乎实现起来更加简洁清晰。然而,缺点也比较明显。TextEditor与UndoManager紧耦合的情况下,如果TextEditor要求也能够实现恢复到前两个状态,此时UndoManager增加了一个undo2的方法,那么TextEditor也需要一并修改。
但是在备忘录模式下,TextEditor相当于至于纯数据类Memento进行交互,面对上面的需求并不需要修改,只需要将上两个的Memento传参即可。
困惑3:在关于备忘录模式的一些材料中,会看到宽接口和窄接口,具体是什么含义?
宽接口指的是Memento备忘录对象提供给Originator访问其内部状态的全部信息,包括私有数据。因为Memento里的数据其实就是Originator中要保存、恢复状态的数据,因此Originator需要能访问到具体的数据信息才可以。
窄接口指的是Memento备忘录对象对Caretaker对象指提供必要的信息进行访问和恢复操作。因为Caretaker对象需要是是Memento对象自身,并不需要访问Memento中的数据,因此称之为窄接口。
困惑4:备忘录模式实现之后,对于调用方的交互似乎变得更加复杂?
一件事情往往有得必有失,很难做到两全其美。为了使得Originator不违背SRP单一职责和OCP开闭原则,Client只能增加交互。
如果在Client和备忘录模式的类之间增加一个中间代理类,这样可以减少与调用方之间的交互,但是代价是又新增一个支撑类。
本质
面向对象程序中,一个类在生命周期过程中,其属性构成的状态是会不断变化的。这种变化会带来很多不确定性,尤其在多线程场景下,可能也会引发一些意想不到的问题。因此,Java语言中经常提倡要利用不变性、局部变量等应对这种不确定性。
然而,在某些现实场景下,类随着时间不断变化是有必要的,并且要求还能沿着时间向后回退。此时,备忘录提供了一种管理对象状态的机制,并且让原有对象维持良好的封装性。