01 意图
Memento是一种行为设计模式,可让您保存和恢复对象的先前状态,而无需透露其实现的细节。
02 问题
想象一下,您正在创建一个文本编辑器应用程序。除了简单的文本编辑之外,您的编辑器还可以格式化文本、插入内嵌图像等。
在某个时候,您决定让用户撤消对文本执行的任何操作。多年来,此功能变得如此普遍,以至于如今人们希望每个应用程序都拥有它。对于实施,您选择采用直接方法。在执行任何操作之前,应用程序会记录所有对象的状态并将其保存在一些存储中。稍后,当用户决定恢复操作时,应用程序会从历史记录中获取最新的快照并使用它来恢复所有对象的状态。
让我们考虑一下这些状态快照。你将如何生产一个?您可能需要检查对象中的所有字段并将它们的值复制到存储中。但是,这只有在对象对其内容的访问限制相当宽松的情况下才有效。不幸的是,大多数真实对象不会让其他人轻易地窥视它们,将所有重要数据隐藏在私有字段中。
现在忽略这个问题,假设我们的对象表现得像嬉皮士:更喜欢开放的关系并保持他们的状态公开。虽然这种方法可以解决眼前的问题并让您随意生成对象状态的快照,但它仍然存在一些严重的问题。将来,您可能会决定重构一些编辑器类,或者添加或删除一些字段。听起来很简单,但这也需要更改负责复制受影响对象状态的类。
[图片上传中...(image-b504b4-1642334969611-2)]
但还有更多。让我们考虑一下编辑器状态的实际“快照”。它包含哪些数据?至少,它必须包含实际文本、光标坐标、当前滚动位置等。要制作快照,您需要收集这些值并将它们放入某种容器中。
最有可能的是,您将在一些代表历史的列表中存储大量这些容器对象。因此,容器可能最终会成为一类的对象。该类几乎没有方法,但有许多反映编辑器状态的字段。要允许其他对象在快照中写入和读取数据,您可能需要公开其字段。这将暴露编辑器的所有状态,无论是否私有。其他类将依赖于快照类的每一个微小变化,否则这些变化将发生在私有字段和方法中,而不会影响外部类。
看起来我们已经走到了死胡同:要么暴露类的所有内部细节,使它们过于脆弱,要么限制对其状态的访问,从而无法生成快照。还有其他方法可以实现“撤消”吗?
03
—
解决方案
我们刚刚遇到的所有问题都是由损坏的封装引起的。有些对象试图做的比他们应该做的更多。为了收集执行某些操作所需的数据,它们侵入了其他对象的私有空间,而不是让这些对象执行实际操作。
Memento 模式将创建状态快照委托给该状态的实际所有者,即创建者对象。因此,与其其他对象试图从“外部”复制编辑器的状态,编辑器类本身可以制作快照,因为它可以完全访问自己的状态。
该模式建议将对象状态的副本存储在一个名为memento的特殊对象中。除了制作它的对象之外,任何其他对象都无法访问备忘录的内容。其他对象必须使用受限接口与备忘录通信,该接口可能允许获取快照的元数据(创建时间、执行操作的名称等),但不能获取快照中包含的原始对象的状态。
这种限制性策略允许您将纪念品存储在其他对象中,通常称为caretakers。由于看守者仅通过有限的接口处理纪念品,因此无法篡改存储在纪念品中的状态。同时,发起者可以访问备忘录中的所有字段,允许其随意恢复之前的状态。
在我们的文本编辑器示例中,我们可以创建一个单独的历史类来充当看守者。每次编辑器即将执行操作时,存储在看守者中的纪念品堆栈都会增长。您甚至可以在应用程序的 UI 中呈现此堆栈,向用户显示以前执行的操作的历史记录。
当用户触发撤消时,历史记录会从堆栈中获取最新的备忘录并将其传递回编辑器,请求回滚。由于编辑器具有对备忘录的完全访问权限,因此它会使用从备忘录中获取的值更改自己的状态。
04 结构实现
基于嵌套类的实现
该模式的经典实现依赖于对嵌套类的支持,嵌套类可用于许多流行的编程语言(如 C++、C# 和 Java)。
基于中间接口的实现
有一个替代实现,适用于不支持嵌套类的编程语言(是的,PHP,我在说你)。
更严格的封装实现
当您不想让其他类通过备忘录访问发起者的状态时,还有一个非常有用的实现。
[图片上传中...(image-c2ad1f-1642334969611-1)]
此示例使用备忘录模式和命令模式来存储复杂文本编辑器状态的快照,并在需要时从这些快照恢复早期状态。
命令对象充当看护者。他们在执行与命令相关的操作之前获取编辑器的备忘录。当用户尝试撤消最近的命令时,编辑器可以使用存储在该命令中的备忘录将自身恢复到先前的状态。
memento 类没有声明任何公共字段、getter 或 setter。因此,任何对象都不能更改其内容。纪念品链接到创建它们的编辑器对象。这允许纪念品通过编辑器对象上的设置器传递数据来恢复链接编辑器的状态。由于纪念品链接到特定的编辑器对象,您可以使您的应用程序支持多个具有集中撤消堆栈的独立编辑器窗口。
// The originator holds some important data that may change over
06 适用场景
- 当您想要生成对象状态的快照以便能够恢复对象的先前状态时,请使用 Memento 模式。
Memento 模式允许您制作对象状态的完整副本,包括私有字段,并将它们与对象分开存储。虽然由于“撤消”用例,大多数人都记得这种模式,但在处理事务时它也是必不可少的(即,如果您需要在错误时回滚操作)。
- 当直接访问对象的字段/getter/setter 违反其封装时使用该模式。
Memento 使对象本身负责创建其状态的快照。没有其他对象可以读取快照,使原始对象的状态数据安全可靠。
07 如何实施
确定哪个类将扮演发起者的角色。重要的是要知道程序是使用这种类型的一个中心对象还是多个较小的对象。
创建纪念品类。一个接一个地声明一组字段,这些字段反映了创建者类中声明的字段。
使 memento 类不可变。纪念品应该通过构造函数只接受一次数据。该类不应该有二传手。
如果您的编程语言支持嵌套类,请将备忘录嵌套在创建者中。如果没有,则从 memento 类中提取一个空白接口,并让所有其他对象使用它来引用 memento。您可以向接口添加一些元数据操作,但不会暴露发起者的状态。
-
将用于制作纪念品的方法添加到 originator 类。发起者应通过备忘录构造函数的一个或多个参数将其状态传递给备忘录。
该方法的返回类型应该是您在上一步中提取的接口(假设您完全提取了它)。在幕后,memento-produce 方法应该直接与 memento 类一起工作。
添加一个用于将发起者的状态恢复到其类的方法。它应该接受一个纪念品对象作为参数。如果您在上一步中提取了接口,请将其设为参数的类型。在这种情况下,您需要将传入的对象类型转换为 memento 类,因为发起者需要对该对象的完全访问权限。
看守者,无论它代表一个命令对象、历史还是完全不同的东西,都应该知道何时向发起者请求新的纪念品、如何存储它们以及何时用特定的纪念品恢复发起者。
看护者和发起者之间的联系可能会移到纪念品类中。在这种情况下,每个纪念品都必须连接到创建它的发起者。恢复方法也将移至 memento 类。然而,只有当 memento 类嵌套在 originator 中或者 originator 类提供了足够的设置器来覆盖其状态时,这一切才有意义。
08 优缺点
[图片上传中...(image-e29769-1642334969614-3)]