设计模式(二十一):备忘录模式(Memento Pattern)

Published on 2024-04-10 17:36 in 分类: 博客 with 狂盗一枝梅
分类: 博客

一、备忘录模式的定义

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。(Without violating encapsulation,capture and externalize an object's internal state so that the object can be restored to this state later.)

玩过Vmware虚拟机的都知道VMware虚拟机有一个“快照”功能,在虚拟操作系统运行过程中,使用该功能能将系统的所有状态保存为一个快照,如果不小心误操作了什么东西,可以根据该快照快速恢复回去。

虚拟机快照功能

和备忘录模式是不是非常像?没错,备忘录模式又被称为“快照模式”,个人而言,我觉得“快照模式”这个名字更贴合它的意思。

快照模式在定义上比较容易理解,接下来看看它的通用类图

备忘录模式-备忘录模式通用类图

备忘录模式类图比较简单

  • 原发器(Originator):记录当前时刻的内部状态,负责定义哪些属于备份范围的状态,负责创建和恢复备忘录数据。
  • 备忘录(Memento):负责存储Originator发起人对象的内部状态,在需要的时候提供发起人需要的内部状态。
  • 备忘录管理员(Caretaker):对备忘录进行管理、保存和提供备忘录。

看到这里,大概有些人会觉得有些熟悉了,它和我们上一节说的 设计模式(二十):命令模式(Command Pattern)似乎有点像,在命令模式中举了个例子是关于文本编辑器的,可以通过命令模式实现操作的可撤销,和这里的备忘录模式很像了,那它们有什么关系?

命令模式是调用者和接受者中间将请求封装了起来,形成一个具体的对象,将对象存到栈里,就可以实现操作的可回溯,之前的例子里只有一个字符串对象backup需要保存,用一个字符串保存下就行,如果想要保存更多呢,甚至完整到整个Editor对象又该怎样做?这就需要使用快照模式了,也即是说使用命令模式完成历史记录,使用快照模式生成快照。

二、备忘录模式案例

本案例使用上一篇 设计模式(二十):命令模式(Command Pattern) 文章中的案例进行改造,使用备忘录模式+命令模式的形式实现新版本的文本编辑器。

改动点主要在Editor类新增一些属性:横纵坐标以及选中文本的宽度,然后是新增一个EditorSnapshot类用于存储Editor快照,Command类则充当快照管理器类。完整类图如下所示

备忘录模式-第 3 页.drawio

相对于命令模式,备忘录模式新增了类EditorSnapshot,Editor类新增了一些属性,并提供了Setter方法,并新增了产生快照的方法;Command类中backup属性字段发生了变化。

1、源码实现

Command类

/**
 * @author kdyzm
 * @date 2024/4/9
 */
public abstract class Command {

    protected Application app;

    protected Editor editor;

    protected EditorSnapshot backup;

    public Command(Application app, Editor editor) {
        this.app = app;
        this.editor = editor;
    }

    /**
     * 备份编辑器状态
     */
    public void saveBackup() {
        backup = editor.createSnapshot();
    }

    /**
     * 恢复编辑器状态
     */
    public void undo() {
        backup.restore();
        System.out.println("执行撤销命令后:" + editor.text);
    }

    /**
     * 执行方法被声明为抽象以强制所有具体命令提供自己的实现。
     * 该方法必须根据命令是否更改编辑器的状态返回 true 或 false。
     */
    abstract boolean execute();
}

Editor类

/**
 * @author kdyzm
 * @date 2024/4/9
 */
public class Editor {

    public String text = "你好";

    private String curX;

    private String curY;

    private Integer selectionWidth;

    public void setText(String text) {
        this.text = text;
    }

    public void setCurX(String curX) {
        this.curX = curX;
    }

    public void setCurY(String curY) {
        this.curY = curY;
    }

    public void setSelectionWidth(Integer selectionWidth) {
        this.selectionWidth = selectionWidth;
    }

    /**
     * 返回选中的文字
     */
    public String getSelection() {
        System.out.println("获取到选中的文字:你好");
        return "你好";
    }


    /**
     * 删除选中的文字
     */
    public void deleteSelection() {
        int index = this.text.lastIndexOf("你好");
        if (index != -1) {
            this.text = this.text.substring(0, index);
            System.out.println("删除选中的文字后:" + this.text);
        }
    }

    /**
     * 在当前位置插入剪切板中的内容
     */
    public void replaceSelection(String text) {
        this.text = this.text + text;
        System.out.println("插入剪切板中的内容后:" + this.text);
    }

    public EditorSnapshot createSnapshot() {
        return new EditorSnapshot(this, text, curX, curY, selectionWidth);
    }
}

EditorSnapshot类

/**
 * @author kdyzm
 * @date 2024/4/10
 */
public class EditorSnapshot {
    
    private Editor editor;

    public String text;

    private String curX;

    private String curY;

    private Integer selectionWidth;

    public EditorSnapshot(Editor editor, String text, String curX, String curY, Integer selectionWidth) {
        this.editor = editor;
        this.text = text;
        this.curX = curX;
        this.curY = curY;
        this.selectionWidth = selectionWidth;
    }

    public void restore() {
        this.editor.setText(this.text);
        this.editor.setCurX(this.curX);
        this.editor.setCurY(this.curY);
        this.editor.setSelectionWidth(this.selectionWidth);
    }
}

CopyCommand类

/**
 * @author kdyzm
 * @date 2024/4/9
 */
public class CopyCommand extends Command {
    public CopyCommand(Application app, Editor editor) {
        super(app, editor);
    }

    /**
     * 复制命令不会被保存到历史记录中,因为它没有改变编辑器的状态。
     */
    @Override
    boolean execute() {
        System.out.println("执行复制命令【start】");
        app.clipboard = editor.getSelection();
        System.out.println("执行复制命令【end】" + "\n");
        return false;
    }
}

CutCommand类

public class CutCommand extends Command {
    public CutCommand(Application app, Editor editor) {
        super(app, editor);
    }

    /**
     * 剪切命令改变了编辑器的状态,因此它必须被保存到历史记录中。
     */
    @Override
    boolean execute() {
        System.out.println("执行剪切命令【start】");
        saveBackup();
        app.clipboard = editor.getSelection();
        editor.deleteSelection();
        System.out.println("执行剪切命令【end】" + "\n");
        return true;
    }
}

PasteCommand类

/**
 * @author kdyzm
 * @date 2024/4/9
 */
public class PasteCommand extends Command {
    public PasteCommand(Application app, Editor editor) {
        super(app, editor);
    }

    @Override
    boolean execute() {
        System.out.println("执行粘贴命令【start】");
        saveBackup();
        editor.replaceSelection(app.clipboard);
        System.out.println("执行粘贴命令【end】" + "\n");
        return true;
    }
}

UndoCommand类

/**
 * 撤销操作也是一个命令
 */
public class UndoCommand extends Command {
    public UndoCommand(Application app, Editor editor) {
        super(app, editor);
    }

    @Override
    boolean execute() {
        System.out.println("执行撤销命令【start】");
        app.undo();
        System.out.println("执行撤销命令【end】" + "\n");
        return false;
    }
}

CommandHistory类

import java.util.Stack;

/**
 * 全局命令历史记录就是一个堆栈
 */
public class CommandHistory {

    private final Stack<Command> history = new Stack<>();

    /**
     * 将命令压入历史记录的末尾
     */
    public void push(Command command) {
        history.push(command);
    }

    /**
     * 从历史记录中取出最近的一条命令
     */
    public Command pop() {
        return history.pop();
    }
}

启动类

/**
 * @author kdyzm
 * @date 2024/4/9
 */
public class Main {

    public static void main(String[] args) {
        //应用程序启动初始化编辑器内文字:你好
        Application app = new Application();
        //复制
        app.doCopy();//你好
        //粘贴
        app.doPaste();//你好你好
        //剪切
        app.doCut();//你好
        //粘贴
        app.doPaste();//你好你好
        //撤销
        app.doUndo();//你好
        //撤销
        app.doUndo();//你好你好


    }
}

运行结果

执行复制命令【start】
获取到选中的文字:你好
执行复制命令【end】

执行粘贴命令【start】
插入剪切板中的内容后:你好你好
执行粘贴命令【end】

执行剪切命令【start】
获取到选中的文字:你好
删除选中的文字后:你好
执行剪切命令【end】

执行粘贴命令【start】
插入剪切板中的内容后:你好你好
执行粘贴命令【end】

执行撤销命令【start】
执行撤销命令后:你好
执行撤销命令【end】

执行撤销命令【start】
执行撤销命令后:你好你好
执行撤销命令【end】


Process finished with exit code 0

和之前是一样的结果。

2、案例总结

备忘录模式是个很简单的模式,它通过创建快照保存当前对象中的数据并可以在未来某一刻恢复回来。

在上面文本编辑器中的案例中,对应着备忘录中的三个角色

  • Originator:对应的类是Editor类
  • Memento:对应的类是EditorSnapshot类
  • Caretaker:对应的类是Command类

需要注意的是EditorSnapshot类的restore方法,该方法其实应该在Editor类中,但是为了不暴露快照对象中的属性,所以将该方法放到了EditorSnapshot方法,EditorSnapshot类仅提供有限的方法用于保存快照和恢复快照。



参考文档:https://refactoringguru.cn/design-patterns/memento



END.


#设计模式
目录