一文彻底搞明白备忘录模式

2024年 5月 16日 85.5k 0

本篇讲解Java设计模式中的备忘录模式,分为定义、模式应用前案例、结构、模式应用后案例、适用场景、模式可能存在的困惑和本质探讨7个部分。

定义

备忘录模式是在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可以将该对象恢复到保存的状态。

在新的分类方式中,备忘录模式被划分至类属性相关需求类别中,其应对的是类的状态属性需要恢复的要求。

一文彻底搞明白备忘录模式-1

模式应用前案例

文本编辑器是一个备忘录模式的典型应用场景。接下来,先来看一下未使用备忘录模式之前的代码实现。

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属性。显然,对于这种类状态(或属性)有变化且能够恢复的场景,应该有更好的解决方案。

结构

一文彻底搞明白备忘录模式-2

备忘录模式的示例实现代码如下。

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 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 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语言中经常提倡要利用不变性、局部变量等应对这种不确定性。

然而,在某些现实场景下,类随着时间不断变化是有必要的,并且要求还能沿着时间向后回退。此时,备忘录提供了一种管理对象状态的机制,并且让原有对象维持良好的封装性。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论