🔥 核心
备忘录模式会捕获一个对象的内部状态,并在该对象之外保存这个状态,
从而可以在以后将对象恢复到原先保存的状态。
🙁 问题场景
你是一名二十年前的海报设计师。最近,一款强大的图形编辑软件(PS)诞生了,你成为了第一批PS试用者。
这个新生的强大工具令你感到惊艳,然而,你发现了一个致命的问题——当你错误执行了一个操作后,只能再次执行一个操作甚至多个操作才能将它的产生的效果抵消掉。
“如果我能撤销这个操作,或者说回退到之前的历史版本就好了”。你自言自语道。
无论是撤销操作,还是回退版本,其实现原理是相同的:程序会记录下对象的当前状态,并将它存储在历史记录中;如果想要使用历史版本,直接根据历史记录修改当前对象的状态即可。被保存的状态信息称为「快照(Snapshot)」。
如何生成一个快照呢?
思路1)遍历对象的所有成员变量,并将其数值复制保存。很遗憾,绝大多数对象会将成员变量设置为私有,并且不会提供对外暴露它的方法。
思路2)假设对象都向嬉皮士那样,喜欢开放的社会关系,因此会公开自己所有的成员变量。但是,这似乎带来了更加严重的安全问题。
我们似乎走进了一条死胡同:要么会限制对其状态的访问权限而无法生成快照,要么会暴露类的所有内部细节而使其过于脆弱…
🙂 解决方案
我们犯了一个致命的错误——总是尝试破坏封装。为什么不尝试从“内部”想想办法呢?
备忘录模式将创建快照的工作放在原对象的内部进行。 一个对象当然拥有对自己完全对访问权限,即使是私有。
我们一只说的“快照”其实是对象的状态信息(成员变量的数值的集合),现在我们将其封装到一个名为 备忘录(Memento)
的特殊对象中;而这一个个 备忘录 ,我们将其放到一个名为 备忘录历史记录(MementoHistory)
的特殊对象中。这两个特殊对象都有一个共同特点:没有任何暴露内部信息的方法。
也就是说,我们捕获对象的内部状态,并在该对象之外保存这个状态;而保存在外部状态并不会暴露自己的信息,在回退状态时,还是由外部对内部的调用实现的。
上面这句话不好理解,下面举一个形象的例子:原对象会把自己的状态作为秘密交给备忘录对象,备忘录对象严格保守这个秘密,不会被任何人得知;因为备忘录对象拥有着指向原对象的引用,所以可以把秘密直接交还给原对象。
恭喜你,已经发现了 Ctrl + z
的秘密!
🌈 有趣的例子
有的时候游戏会出现地图板块缺失、装备参数异常、物品无法掉落等恶心BUG。这时,便需要立即回退游戏版本。
游戏(Game)
本身对应原对象,游戏版本(Version)
对应备忘录,游戏版本历史记录(VersionHistory)
对应备忘录历史记录。明确了这个结构,游戏版本回退功能其实不难实现。
游戏
class Game {
// 版本信息
private int versionId;
private String versionName;
public void setVersionId(int versionId) { this.versionId = versionId; }
public void setVersionName(String versionName) { this.versionName = versionName; }
// 生成版本快照(关键)
public Version createVersion() {
return new Version(this, versionId, versionName);
}
public void showVersion() {
System.out.println("【" + versionId + "】" + versionName);
}
}
游戏版本
class Version {
// 指向原对象的引用
Game game;
// 用于回退原对象版本的信息
private int versionId;
private String versionName;
public Version(Game game, int versionId, String versionName) {
this.game = game;
this.versionId = versionId;
this.versionName = versionName;
}
// 回退原对象版本(关键)
public void rollbackVersion() {
game.setVersionId(versionId);
game.setVersionName(versionName);
}
}
游戏版本历史记录
class VersionHistory {
// 这是一个栈数据结构
private Deque<Version> versionStack = new ArrayDeque<>();
// 某一版本入栈
public void push(Version version) {
versionStack.push(version);
}
// 出栈某一版本
public Version pop() {
return versionStack.pop();
}
}
public class MementoPatternDemo {
public static void main(String[] args) {
// 创建一个游戏对象(原对象)
Game game = new Game();
// 创建一个游戏版本历史记录对象(备忘录历史记录对象)
VersionHistory versionHistory = new VersionHistory();
// 设置当前游戏版本,并将版本快照(备忘录)入栈
game.setVersionId(100);
game.setVersionName("version-100");
game.showVersion();
versionHistory.push(game.createVersion());
// 设置当前游戏版本,并将版本快照(备忘录)入栈
game.setVersionId(200);
game.setVersionName("version-200");
game.showVersion();
versionHistory.push(game.createVersion());
// 设置当前游戏版本,并将版本快照(备忘录)入栈
game.setVersionId(300);
game.setVersionName("version-300");
game.showVersion();
versionHistory.push(game.createVersion());
// 版本快照(备忘录)出栈,用于回退游戏版本
versionHistory.pop().rollbackVersion();
game.showVersion();
// 版本快照(备忘录)出栈,用于回退游戏版本
versionHistory.pop().rollbackVersion();
game.showVersion();
// 版本快照(备忘录)出栈,用于回退游戏版本
versionHistory.pop().rollbackVersion();
game.showVersion();
}
}
【100】version-100
【200】version-200
【300】version-300
【300】version-300
【200】version-200
【100】version-100
☘️ 使用场景
◾️当你需要创建对象状态快照来恢复其之前的状态时,可以使用备忘录模式。
备忘录模式允许你复制对象中的全部状态(包括私有成员变量),并将其独立于对象进行保存。尽管大部分人因为“撤销”这个用例才记得该模式,但其实它在处理事务(比如需要在出现错误时回滚一个操作)的过程中也必不可少。
◾️当直接访问对象的成员变量、获取器或设置器将导致封装被突破时,可以使用该模式。
备忘录让对象自行负责创建其状态的快照。任何其他对象都不能读取快照,这有效地保障了数据的安全性。
🧊 实现方式
(1)确定担任原发器角色的类。重要的是明确程序使用的一个原发器中心对象,还是多个较小的对象。
(2)创建备忘录类。逐一声明对应每个原发器成员变量的备忘录成员变量。
(3)将备忘录类设为不可变。备忘录只能通过构造函数一次性接收数据。该类中不能包含设置器。
(4)如果你所使用的编程语言支持嵌套类,则可将备忘录嵌套在原发器中;如果不支持,那么你可从备忘录类中抽取一个空接口,然后让其他所有对象通过接口来引用备忘录。你可在该接口中添加一些元数据操作,但不能暴露原发器的状态。
(5)在原发器中添加一个创建备忘录的方法。原发器必须通过备忘录构造函数的一个或多个实际参数来将自身状态传递给备忘录。
(6)该方法返回结果的类型必须是你在上一步中抽取的接口(如果你已经抽取了)。实际上,创建备忘录的方法必须直接与备忘录类进行交互。
(7)在原发器类中添加一个用于恢复自身状态的方法。该方法接受备忘录对象作为参数。如果你在之前的步骤中抽取了接口,那么可将接口作为参数的类型。在这种情况下,你需要将输入对象强制转换为备忘录,因为原发器需要拥有对该对象的完全访问权限。
(8)无论负责人是命令对象、历史记录或其他完全不同的东西,它都必须要知道何时向原发器请求新的备忘录、如何存储备忘录以及何时使用特定备忘录来对原发器进行恢复。
(9)负责人与原发器之间的连接可以移动到备忘录类中。在本例中,每个备忘录都必须与创建自己的原发器相连接。恢复方法也可以移动到备忘录类中,但只有当备忘录类嵌套在原发器中,或者原发器类提供了足够多的设置器并可对其状态进行重写时,这种方式才能实现。
🎲 优缺点
➕ 你可以在不破坏对象封装情况的前提下创建对象状态快照。
➕ 你可以通过让负责人维护原发器状态历史记录来简化原发器代码。
➖ 如果客户端过于频繁地创建备忘录,程序将消耗大量内存。
➖ 负责人必须完整跟踪原发器的生命周期,这样才能销毁弃用的备忘录。
➖ 绝大部分动态编程语言(例如 PHP、Python和JavaScript)不能确保备忘录中的状态不被修改。
🌸 补充
备忘录(Memento)
可以是原发器的内部类,这样逻辑会更加清晰。
备忘录历史记录(MementoHistory)
就是个备忘录管理类,可有可无,加上更符合迪米特法则。