【Design Pattern】Behavioural - Memento

Posted by 西维蜀黍 on 2018-11-11, Last Modified on 2022-12-10

1 动机

备忘录模式(Memento Pattern),又称为快照模式(Snapshot Pattern),为对象提供了恢复到先前状态的能力(通过回滚(rollback))。

一个典型的例子是文本编辑器(text editor)可以在任何时刻保存,并且使用撤销(undo)来撤回到先前的保存状态。

2 定义

**备忘录模式(Memento Pattern)**是在不破坏封装的前提下,获取一个对象的内部状态,并将这个状态保存在这个对象之外,以实现在未来将对象恢复到先前状态的一种模式。

常见的软件系统往往不止存储一个状态,而是需要存储多个状态。这些状态常常是一个对象历史发展的不同阶段的快照,存储这些快照的备忘录对象叫做对象的历史;某一个快照所处的位置叫做检查点(Check Point)

3 构成

  • 发起者角色(Originator):它的状态会被保存起来。而保存机制通过使用一个内部类(inner class)来实现,这个内部类就是备忘录角色(Memento),备忘录角色被定义在发起者内部。
  • 负责者角色(Caretaker):帮助存储和恢复发起者的状态。Caretaker会在内部维护一个状态池(用于保存每一次发起者的状态)。
  • 备忘录角色(Memento):每一次保存,发起者的状态都会被保存在一个备忘录角色的实例中。它是一个内部类,因此它不能被(除发起者外的)任何类访问。

交互

  • 每一次调用负责者以保存Originator的状态,负责者都会将FileWriterUtil的当前状态存储于一个备忘录角色对象中。若保存了5次,则会有5个备忘录角色实例。
  • 每一次调用负责者以撤回,都会从在状态池中获取先前存储的Memento对象(表示当时的那个状态),并改写Originator对象。

UML图

发起者角色(Originator)

class Originator {
    private String state;

    public String getState() {
        return state;
    }

    public void setState(String state) {
        this.state = state;
    }

    public Originator(String state) {
        this.state = state;
    }
    
    /**
     * 工厂方法,返还一个新的备忘录对象
     */

    public Memento createMemento() {
        return new Memento(this.state);
    }

    public void restoreMemento(Memento memento) {
        Memento m = (Memento) memento;
        this.setState(m.getSavedState());
    }
}

负责者角色(Caretaker)

备忘录角色(Memento)

4 示例

我们实现一个可以撤回的文本编辑器以作为例子。每次保存时,都会将文本内容写入文件中。

注意,为了尽可能简化实现而突出问题实质,将文本写入文件的逻辑在以下实现中省略了。

UML图

实现思路

  • FileWriterCaretaker 类对应Caretaker角色,为调用者提供接口,用于保存或撤回(恢复先前状态)。
  • FileWriterUtil表示当前文本
  • Memento表示每一次保存时,文本的状态

Originator 和内部类Memento

class FileWriterUtil {

    private String fileName;
    private StringBuilder content;

    public FileWriterUtil(String file) {
        this.fileName = file;
        this.content = new StringBuilder();
    }

    @Override
  public String toString() {
        return this.content.toString();
    }

    public void write(String str) {
        content.append(str);
    }

    public Memento save() {
        return new Memento(this.fileName, this.content);
    }

    public void undoToLastSave(Object obj) {
        Memento memento = (Memento) obj;
        this.fileName = memento.fileName;
        this.content = memento.content;
    }

    private class Memento {
        private String fileName;
        private StringBuilder content;

        public Memento(String file, StringBuilder content) {
            this.fileName = file;
            //notice the deep copy so that Memento and FileWriterUtil content variables don't refer to same object
  this.content = new StringBuilder(content);
        }
    }
}

Caretaker

class FileWriterCaretaker {

    private Object obj;

    public void save(FileWriterUtil fileWriter) {
        this.obj = fileWriter.save();
    }

    public void undo(FileWriterUtil fileWriter) {
        fileWriter.undoToLastSave(obj);
    }
}

调用者调用代码

public class Client {

    public static void main(String[] args) {

        FileWriterCaretaker caretaker = new FileWriterCaretaker();

        FileWriterUtil fileWriter = new FileWriterUtil("data.txt");
        fileWriter.write("First Set of Data\n");
        System.out.println(fileWriter + "\n");

        // lets save the file
        caretaker.save(fileWriter);
        //now write something else
        fileWriter.write("Second Set of Data\n");

        //checking file contents
        System.out.println(fileWriter + "\n");

        //lets undo to last save
        caretaker.undo(fileWriter);

        //checking file content again
        System.out.println(fileWriter + "\n");

    }
}

输出

/* output:
First Set of Data

First Set of Data
Second Set of Data

First Set of Data
*/

分析

  • FileWriterUtil对象表示当前的文本
  • 每一次调用FileWriterCaretaker 以保存FileWriterUtil的状态,FileWriterCaretaker 都会将FileWriterUtil的当前状态存储于一个Memento对象中。若保存了5次,则会有5个Memento实例
  • 每一次调用Caretaker以撤回,都会从在状态池中获取先前存储的Memento对象(表示当时的那个状态),并改写FileWriterUtil对象
  • 在上面例子的Caretaker中,private Object obj;的声明决定了只会恢复到上一次的状态(撤销一次)。若希望允许多次撤销),则可以修改为private List<Object> obj;以保存多次状态
  • 在上面的例子中,我们采用深拷贝(deep copy)的方式来保存文本的状态,以避免数据完整性问题
  • 在上面的例子中,我们简单粗暴地采取了全量存储状态的方式,若当前文本对象较大,这样的做法是极为消耗内存的。取而代之,我们可以采取增量存储的方式来作为实现的优化

模式的优缺点

优点

  • 给用户提供了一种可以恢复状态的机制。可以是用户能够比较方便地回到某个历史的状态。
  • 实现了信息的封装。使得用户不需要关心状态的保存细节。

缺点

消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。

模式适用场景

  1. 需要保存一个对象在某一个时刻的状态或部分状态。
  2. 如果用一个接口来让其他对象得到这些状态,将会暴露对象的实现细节并破坏对象的封装性,一个对象不希望外界直接访问其内部状态,通过负责人可以间接访问其内部状态。

Reference