备忘录(Memento)模式

备忘录模式是一种对象行为型设计模式,用于在不破坏封装性的情况下,捕获对象的内部状态并在需要时恢复对象到原先保存的状态。适用于需要记录对象内部状态以支持撤销/重做操作或错误恢复的场景。备忘录(Memento)存储原发器(Originator)对象的状态,由负责人(Caretaker)管理,确保只有原发器能访问备忘录的内部状态。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

备忘录(Memento)模式

隶属类别——对象行为型


1. 意图

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

2. 别名

Token

3. 动机

有时有必要记录一个对象的内部状态。为了允许用户取消不确定的操作或从错误中恢复过来,需要实现检查点和取消机制,而要实现这些机制,你必须事先将状态信息保存在某处,这样才能将对象恢复到它们先前的状态。但是对象通常封装了其部分或所有的状态信息,使得其状态被其他对象访问,也就不可能在该对象之外保存其状态。而暴露其内部状态又将违反封装的原则,可能有损应用的可靠性和可扩展性。

例如,考虑一个图形编辑器,它支持图形对象间的连线。用户可用一条直线连接两个矩形,而当用户移动任意一个矩形时,这两个矩形仍能保持连接。在移动过程中,编辑器自己伸展这条直线以保持该连接。

在这里插入图片描述

一个众所周知的保持对象间连接关系的方法是使用一个约束解释系统。我们可将一个功能封装在一个ConstriantSolver对象中。ConstriantSolver在连接生成时,记录这些连接并产生描述它们的数学方程。当用户生成一个连接或者修改图形时,ConstriantSlover就求解这些方程。并根据它的计算结果重新调整图像,使各个对象保持正确的连接。

在这应用中,支持取消操作并不像看起来那么容易。一个显而易见的方法是,每次移动时保持移动的距离,而在起取消这次移动时该对象移回相等的距离。然而,这不能保持所有的对象都会出现在它们原先出现的地方。设想在移动过程某连接中一些松弛。在这种情况下,简单地将矩形移回它原来的位置并不一定能得到预想的结果。

在这里插入图片描述

一般来说,ConstraintSolver的公共接口可能不足以精确地逆转它对其他对象的作用。为重建先前的状态,取消操作机制必须与ConstraintSolver更紧密的结合,但我们同时也应避免将ConstraintSolver的内部暴露给取消操作机制。

我们可用Mement(备忘录)模式解决这一问题。一个Mement(备忘录)是一个对象,它存储另一个对象在某个瞬间的内部状态,而后者称为备忘录的原发器(originator)。当需要设置原发器的检查点时,取消操作机制会想原发器相求一个备忘录。原发器用描述当前状态的信息初始化该备忘录。只有原发器可以向备忘录中存取信息,备忘录对其他的对象“不可见”。

在刚才的讨论的图形编辑器的例子中,ConstraintSolver可作为一个原发器。下面的事件序列描述了取消操作的过程:

  • 1)作为移动操作的一个副作用,编辑器向ConstraintSolver请求一个备忘录。
  • 2)ConstraintSolver创建并返回一个备忘录,在这个例子中该备忘录是SolverState类的一个实例。SolverState备忘录包含一些描述ConstraintSolver的内部等式和变量当前状态的数据结构。
  • 3)此后当用户取消移动操作时,编辑器将SolverState备忘录送回给ConstraintSolver。
  • 4)根据SolverState备忘录中的信息,ConstraintSolver改变它的内部结构以精确地将它的等式和变量返回到它们各自先前的状态。

这一方案允许ConstraintSolver把恢复先前状态所需的信息交给其他的对象,而又不暴露它的内部结构和表示。

4. 适用性

在以下情况下使用备忘录模式:

  • 必须保存一个对象在某个时刻的(部分)状态,这样以后需要时它才能恢复到先前的状态。
  • 如果一个用接口来让其他对象直接得到这些状态,将会暴露对象的实现细节并破坏对象的封装性。

5. 结构

在这里插入图片描述

6. 参与者

  • Memento(备忘录,如SolverState)
    • 备忘录存储原发器对象的内部状态。原发器根据需要决定备忘录存储原发器的哪些内部状态。
    • 防止原发器以外的其他对象访问备忘录。备忘录实际上有两个接口,管理者(caretaker)只能看到备忘录的接口——它只能将备忘录传递给其他对象。相反,原反器能看到一个接口,允许它访问返回到先前状态所需的所有数据。理想的情况时只允许生成备忘录的那个原发器访问本备忘录的内部状态。
  • Originator(原发器,如ConstraintSolver)
    • 原发器创建一个备忘录,用以当前时刻它的内部状态。
    • 使用备忘录恢复内部状态。
  • Caretaker(负责人,如undo mechanism)
    • 负责保存好备忘录。
    • 不能对备忘录的内容进行操作或检查。

7. 协作

  • 管理者向原发器请求一个备忘录,保留一段时间后,将其送回给原发器,如下面的交互图所示。

在这里插入图片描述

有时管理者不会将备忘录返回给原发器,因为原发器可能根本不需要退到先前的状态。

  • 备忘录是被动的。只有创建(修改)备忘录的原发器会对它的状态进行赋值和检索。

8. 效果

备忘录有以下一些优点:

  • 1)保持封装边界 使用备忘录可以避免暴露一些只应有原发器管理却又必须存储在原发器之外的信息。该模式把可能的很复杂的Originator内部信息对其他对象屏蔽起来,从而保持了封装边界。
  • 2)它简化了原发器。 在其他保持封装性的设计中,Originator负责保持客户请求过的内部的状态版本。这就把所有存储管理的重任交给了Originatior。让客户管理它们请求的状态将会简化Originator,并且使得客户工作结束时无需通知原发器。

缺点:

  • 1)使用备忘录可能代价很高 如果原发器在生成备忘录时必须拷贝并存储大量的信息,或者客户非常频繁地创建和恢复原发器状态,可能会导致非常大量的开销。除非封装和恢复Originator状态的开销不大,否则该模式可能并不适合。参见实现一节中关于增量式改变的讨论。
  • 2)定义窄接口和宽接口 在一些语言中可能难以保证只有原发器可访问备忘录的状态。Java可以通过内部类的方式实现。
  • 3)维护备忘录的潜在代价 管理器负责删除它所维护的备忘录。然而,管理器不知道备忘录中有多少个状态。因此当存储备忘录时,一个本来很大小的管理器,可能会产生大量的存储开销。

9. 实现

下面是当实现备忘录模式时应该考虑的两个问题:

  • 1)语言支持

  • 2)存储增量改变 如果备忘录的创建及其返回(给它们的原发器)的顺序时可预测的,备忘录可以仅存储原发器内部状态的增量改变

    例如,一个包含可撤销的Command(命令)的历史列表可使用备忘录以保证当命令被取消时,它们可以被恢复到正确的状态。 历史列表定义了一个特定的顺序,按照这个顺序名字可以被取消和重做。这意味着备忘录可以只村吃一个命令所产生的增量改变而不是它所影响的每一个对象的完整状态。在前面动机可以仅存储那些变化了的内部结构,以保持直线和矩形相连,而不是存储这些对象的绝对位置。

10. 代码示例

首先是Originator & Memento(Java中实现Memento模式可以使用内部类)——FileWriteUtil.java

public class FileWriterUtil {
	
	private String fileName;
	private StringBuilder content;
	
	public FileWriterUtil(String title) {
		this.fileName = title;
		content = new StringBuilder();
	}
	
	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;
	}
	
	@Override
	public String toString() {
		return this.content.toString();
	}
	
	private class Memento{
		private String fileName;
		private StringBuilder content;
		
		public Memento(String file, StringBuilder content) {
			this.fileName = file;
			// notice the deeply copy so that Memento and FileWriterUtil content variables don't refer to same
			// object
			this.content = new StringBuilder(content);
		}
	}
}

接下来是Caretaker——FileWriteCaretaker.java

public class FileWriterCaretaker {

	private Object obj;
	
	public void save(FileWriterUtil fileWriter) {
		this.obj = fileWriter.save();
	}
	
	public void undo(FileWriterUtil fileWriter) {
		fileWriter.undoToLastSave(obj);
	}
}

接下来是Client——FileWriterClient.java

public class FileWriterClient {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		
		FileWriterCaretaker caretaker = new FileWriterCaretaker();
		
		FileWriterUtil fileWriter = new FileWriterUtil("data.txt");
		fileWriter.write("First Set of Data\n");
		System.out.println(fileWriter+"\n\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\n");

		//lets undo to last save
		caretaker.undo(fileWriter);
		
		//checking file content again
		System.out.println(fileWriter+"\n\n");
		
		fileWriter.write("This is Memento Pattern\n");
		System.out.println(fileWriter);
		fileWriter.write("I am 饭团小神\n");
		System.out.println(fileWriter);
		caretaker.save(fileWriter);
		fileWriter.write("I am fine");
		System.out.println(fileWriter+"\n\n");
		caretaker.undo(fileWriter);
		System.out.println(fileWriter);
	}

}

对于的实现结果:

First Set of Data



First Set of Data
Second Set of Data



First Set of Data



First Set of Data
This is Memento Pattern

First Set of Data
This is Memento Pattern
I am 饭团小神

First Set of Data
This is Memento Pattern
I am 饭团小神
I am fine


First Set of Data
This is Memento Pattern
I am 饭团小神

最后附上类的UML图:

在这里插入图片描述

11. 相关应用

Dylan中的Collection提供了一个反映备忘录模式的迭代接口。Dylan的集合有一个“状态”对象的概念,它是一个表示迭代状态的备忘录。每一个集合可以按照它所选择的任意方式表示迭代的当前状态;该表示对客户完全不可见,Dylan的迭代方法。在Java也有,其具体情况如下:

好像算不上。

    private class Itr implements Iterator<E> {
        /**
         * Index of element to be returned by subsequent call to next.
         */
        int cursor = 0;

        /**
         * Index of element returned by most recent call to next or
         * previous.  Reset to -1 if this element is deleted by a call
         * to remove.
         */
        int lastRet = -1;

        /**
         * The modCount value that the iterator believes that the backing
         * List should have.  If this expectation is violated, the iterator
         * has detected concurrent modification.
         */
        int expectedModCount = modCount;

        public boolean hasNext() {
            return cursor != size();
        }

        public E next() {
            checkForComodification();
            try {
                int i = cursor;
                E next = get(i);
                lastRet = i;
                cursor = i + 1;
                return next;
            } catch (IndexOutOfBoundsException e) {
                checkForComodification();
                throw new NoSuchElementException();
            }
        }

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                AbstractList.this.remove(lastRet);
                if (lastRet < cursor)
                    cursor--;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException e) {
                throw new ConcurrentModificationException();
            }
        }

        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
    }

基于备忘录的迭代接口有两个有趣的优点:

    1. 在同一个集合上可有多个状态一起工作。
    1. 它不需要为支持迭代而破坏一个集合的封装性。 备忘录仅有集合自身来解释;任何其他对象都不能访问它,支持迭代的其他的方法将迭代器作为他们集合类的友元,从而破坏了封装性。这一情况在基于备忘录实现中不再存在,此时Collection是Iterator的一个友元。

    QOCA约束解释工具在备忘录中存储增量信息。客户可得到刻画某约束系统当前解释的备忘录。该备忘录仅包括从上一次解释以来发生改变的那些约束变量。通常每次新的解释仅有一小部分解释器变量发生改变。这个发生变化的变量子集已足以将解释器恢复到先前的解释;恢复更前的解释要求经过中间的解释逐步恢复,所以不能以任意的顺序设定备忘录;QOCA依赖一种历史机制来恢复到先前的解释。

12. 相关模式

  • Command: 命令可使用Memento来为可撤销的操作维护状态。
  • Iterator: 如前所述,Memento可用于迭代。

13. 设计原则口袋

  • 封装变化
  • 多用组合,少用继承
  • 对交互对象间的松耦合设计而努力
  • 类应该对扩展开放,对修改关闭
  • 针对接口编程,不针对实现编程
  • 依赖抽象,不要依赖具体类
  • 只和密友交谈
  • 好莱坞原则——别来找我,我会来找你
  • 单一责任原则——类应该只有一个改变的理由

14. 参考文献

《设计模式:可复用面向对象软件的基础》

《HeadFirst设计模式》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值