纪念品
意图
纪念品是一种行为设计模式,它允许您保存和恢复对象以前的状态,而不透露其实现的细节。

问题
想象一下,你正在创建一个文本编辑器应用程序。除了简单的文本编辑,你的编辑器可以格式化文本,插入内联图像等。
在某些时候,您决定让用户撤消对文本执行的任何操作。多年来,这个功能已经变得非常普遍,现在人们希望每个应用程序都有这个功能。对于实现,您选择了直接方法。在执行任何操作之前,应用程序会记录所有对象的状态,并将其保存在某个存储空间中。稍后,当用户决定恢复一个操作时,应用程序从历史记录中获取最新的快照,并使用它来恢复所有对象的状态。

在执行操作之前,应用程序会保存对象状态的快照,稍后可以使用该快照将对象恢复到之前的状态。
让我们考虑一下这些状态快照。你到底要怎么制作呢?您可能需要遍历对象中的所有字段,并将它们的值复制到存储中。然而,只有当对象对其内容的访问限制相当宽松时,这才会起作用。不幸的是,大多数真实对象不允许其他人轻易地查看它们内部,将所有重要数据隐藏在私有字段中。
现在先忽略这个问题,让我们假设我们的对象像嬉皮士一样:喜欢开放关系并保持它们的状态是公共的。虽然这种方法可以解决眼前的问题,并允许您随意生成对象状态的快照,但它仍然存在一些严重的问题。将来,您可能决定重构一些编辑器类,或者添加或删除一些字段。听起来很简单,但这也需要更改负责复制受影响对象状态的类。

如何复制对象的私有状态?
但还有更多。让我们考虑编辑器状态的实际“快照”。它包含哪些数据?至少,它必须包含实际文本、光标坐标、当前滚动位置等。要创建快照,您需要收集这些值并将它们放入某种容器中。
最有可能的是,您将在某个表示历史的列表中存储大量这样的容器对象。因此,容器可能最终成为一个类的对象。这个类几乎没有方法,但是有很多字段反映编辑器的状态。为了允许其他对象向快照读写数据,您可能需要将其字段设为公共。这将暴露编辑器的所有状态(私有或非私有)。其他类将依赖于对快照类的每一个小更改,否则这些更改将发生在私有字段和方法中,而不会影响外部类。
看起来我们已经走到了死胡同:要么暴露类的所有内部细节,使它们过于脆弱,要么限制对它们状态的访问,从而无法生成快照。还有其他方法来实现“撤销”吗?
解决方案
我们刚才遇到的所有问题都是由破碎的封装引起的。一些对象尝试做比它们应该做的更多的事情。为了收集执行某些操作所需的数据,它们会侵入其他对象的私有空间,而不是让这些对象执行实际操作。
Memento模式将创建状态快照委托给该状态的实际所有者发起者对象。因此,与其他对象试图从“外部”复制编辑器的状态不同,编辑器类本身可以创建快照,因为它可以完全访问自己的状态。
该模式建议将对象状态的副本存储在名为纪念品.除了产生纪念品的物体,其他物体无法访问纪念品的内容。其他对象必须使用有限的接口与内存进行通信,该接口可能允许获取快照的元数据(创建时间、执行操作的名称等),但不允许获取快照中包含的原始对象的状态。

发起者拥有对纪念品的完全访问权,而保管者只能访问元数据。
这种限制性策略允许您在其他对象中存储纪念品,通常称为看护人.由于管理员只能通过有限的接口来处理纪念品,所以它不能篡改存储在纪念品中的状态。同时,发起者可以访问纪念品内的所有字段,允许它随意恢复以前的状态。
在我们的文本编辑器示例中,我们可以创建一个单独的历史类来充当管理员。每当编辑器要执行一个操作时,存储在管理器中的一堆纪念品就会增加。您甚至可以在应用程序的UI中呈现这个堆栈,向用户显示以前执行的操作的历史。
当用户触发撤销时,历史记录从堆栈中抓取最近的纪念品并将其传递给编辑器,请求回滚。由于编辑器拥有对纪念品的完全访问权,因此它会使用从纪念品中获取的值来更改自己的状态。
结构
基于嵌套类的实现
模式的经典实现依赖于对嵌套类的支持,许多流行的编程语言(如c++、c#和Java)都支持嵌套类。


的发起者类可以生成其自身状态的快照,也可以在需要时从快照恢复其状态。
的纪念品是一个值对象,用作发起者状态的快照。通常的做法是使记忆件不可变,并通过构造函数只向它传递一次数据。
的看守不仅知道“何时”和“为什么”捕获发起者的状态,还知道何时应该恢复状态。
管理员可以通过存储一堆纪念品来跟踪创始人的历史。当发起者必须回到历史中去时,管理员从堆栈中取出最上面的纪念品,并将其传递给发起者的恢复方法。
在这个实现中,memento类嵌套在发起者内部。这使得发起者可以访问纪念品的字段和方法,即使它们被声明为私有。另一方面,管理员对纪念品的字段和方法的访问非常有限,这使得它可以将纪念品存储在堆栈中,但不能篡改它们的状态。
基于中间接口的实现
有一种替代实现,适用于不支持嵌套类的编程语言(是的,PHP,我说的就是你)。


在没有嵌套类的情况下,可以通过建立一个约定来限制对纪念品字段的访问,该约定规定管理员只能通过显式声明的中间接口来使用纪念品,该接口只声明与纪念品元数据相关的方法。
另一方面,发起者可以直接使用memento对象,访问在memento类中声明的字段和方法。这种方法的缺点是需要将纪念品的所有成员声明为public。
使用更严格的封装实现
当您不想让其他类通过纪念品访问发起者的状态时,还有另一种实现非常有用。


这种实现允许有多种类型的发起者和纪念品。每个创始者都有相应的纪念物类。发起者和纪念品都不会向任何人暴露他们的状态。
管理人员现在被明确限制不能更改存储在纪念品中的状态。此外,由于恢复方法现在定义在memento类中,所以管理员类独立于发起者。
每件纪念品都与制造它的人联系在一起。发起者将自身传递给纪念品的构造函数,以及它的状态值。由于这些类之间的密切关系,纪念品可以恢复其创建者的状态,前提是后者已经定义了适当的setter。
伪代码
这个例子使用了Memento模式和命令模式,用于存储复杂文本编辑器状态的快照,并在需要时从这些快照恢复较早的状态。

保存文本编辑器状态的快照。
命令对象充当监护器。它们在执行与命令相关的操作之前获取编辑器的纪念品。当用户试图撤销最近的命令时,编辑器可以使用该命令中存储的记忆项将自己恢复到以前的状态。
memento类没有声明任何公共字段、getter或setter。因此,任何对象都不能改变其内容。纪念品链接到创建它们的编辑器对象。这让记忆体通过编辑器对象上的setter传递数据来恢复链接编辑器的状态。由于纪念品链接到特定的编辑器对象,你可以让你的应用程序支持几个独立的编辑器窗口与一个集中的撤销堆栈。
//发起者持有一些重要的数据,这些数据可能会随着时间的推移而改变。它还定义了一个方法用于将其状态保存在//纪念品中,以及另一个方法用于从中恢复状态。类编辑器是私有字段文本,curX, curY, selectionWidth方法setText(文本)就是这个。方法setCursor(x, y)是这样的。curX = x这个。curY = y方法setSelectionWidth(width)是这个。selectionWidth = width //将当前状态保存在一个纪念品中。method createSnapshot():快照是// Memento是一个不可变的对象;这就是为什么//发起者将其状态传递给纪念品的//构造函数参数。//记忆块类存储编辑器的过去状态。快照(编辑器,文本,curX, curY, selectionWidth)是这个。编辑=编辑这个。Text = Text this。curX = x这个。curY = y这个。selectionWidth = selectionWidth //在某些情况下,编辑器的前一个状态可以使用纪念品对象恢复。 method restore() is editor.setText(text) editor.setCursor(curX, curY) editor.setSelectionWidth(selectionWidth) // A command object can act as a caretaker. In that case, the // command gets a memento just before it changes the // originator's state. When undo is requested, it restores the // originator's state from a memento. class Command is private field backup: Snapshot method makeBackup() is backup = editor.createSnapshot() method undo() is if (backup != null) backup.restore() // ...
适用性
当您希望生成对象状态的快照以恢复对象以前的状态时,请使用Memento模式。
Memento模式允许您对对象的状态(包括私有字段)进行完整的复制,并将它们与对象分开存储。虽然由于“撤销”用例,大多数人都记得这种模式,但在处理事务时(即,如果您需要在错误时回滚操作),它也是必不可少的。
当直接访问对象的fields/getters/setters违反其封装时,请使用该模式。
Memento使对象本身负责创建其状态的快照。没有其他对象可以读取快照,使得原始对象的状态数据安全可靠。
如何实施
确定哪个类将扮演发起者的角色。了解程序是使用该类型的一个中心对象还是多个较小的对象是很重要的。
创建memento类。一个接一个地声明一组字段,这些字段反映了在originator类中声明的字段。
使memento类不可变。记忆体应该只通过构造函数接受一次数据。类不应该有setter。
如果您的编程语言支持嵌套类,请将纪念品嵌套在发起者中。如果不是,则从memento类中提取一个空白接口,并使所有其他对象使用它来引用该memento。您可以向接口添加一些元数据操作,但不能公开发起者的状态。
向originator类添加一个生成纪念品的方法。发起者应该通过纪念品构造函数的一个或多个参数将其状态传递给纪念品。
方法的返回类型应该是您在前一步中提取的接口(假设您已经提取了它)。实际上,纪念品生产方法应该直接与纪念品类一起工作。
添加用于将发起者的状态恢复到其类的方法。它应该接受一个memento对象作为参数。如果在上一步中提取了接口,则将其作为参数的类型。在这种情况下,需要将传入对象类型转换为memento类,因为发起者需要对该对象的完全访问权。
管理员(无论它代表命令对象、历史记录还是完全不同的东西)应该知道何时向创建者请求新的纪念品、如何存储它们以及何时使用特定的纪念品恢复创建者。
保管者和始发者之间的联系可以转移到纪念品类。在这种情况下,每件纪念品都必须与创造它的人联系在一起。恢复方法也将移动到memento类。然而,只有当memento类嵌套到originator类中,或者originator类提供了足够的setter来覆盖其状态时,这才有意义。
利与弊
- 您可以生成对象状态的快照,而不违反其封装。
- 您可以通过让管理员维护创建者状态的历史记录来简化创建者的代码。
- 如果客户端过于频繁地创建纪念品,应用程序可能会消耗大量内存。
- 管理人员应该跟踪发起者的生命周期,以便能够销毁过时的纪念品。
- 大多数动态编程语言,如PHP、Python和JavaScript,都不能保证记忆体中的状态保持不变。