备忘录模式是一种行为设计模式, 允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。
亦称: 快照、Snapshot、Memento
备忘录模式结构
基于嵌套类的实现
该模式的经典实现方式依赖于许多流行编程语言 (例如 C++、 C# 和 Java) 所支持的嵌套类。
基于中级接口的实现
另外一种实现方法适用于不支持嵌套类的编程语言 (没错, 我说的就是 PHP)。
封装更严格的实现
如果你不想让其他类有任何机会通过备忘录来访问原发器的状态, 那么还有另一种可用的实现方式。
样例
备忘录模式记录配置文件版本信息
ConfigFile
package behavioral.memento;
import java.util.Date;
/**
* 配置信息类
*/
public class ConfigFile {
private String versionNo; //版本号
private String content; //内容
private Date dateTime; //时间
private String operator; //操作人
public ConfigFile(String versionNo, String content, Date dateTime, String operator) {
this.versionNo = versionNo;
this.content = content;
this.dateTime = dateTime;
this.operator = operator;
}
//get方法
}
ConfigMemento
package behavioral.memento;
/**
* 备忘录类
*/
public class ConfigMemento {
private ConfigFile configFile;
public ConfigMemento(ConfigFile configFile) {
this.configFile = configFile;
}
public ConfigFile getConfigFile() {
return configFile;
}
}
ConfigOriginator
package behavioral.memento;
/**
* 记录者类
*/
public class ConfigOriginator {
private ConfigFile configFile;
public ConfigFile getConfigFile() {
return configFile;
}
public void setConfigFile(ConfigFile configFile) {
this.configFile = configFile;
}
/**
* 保存的时候会直接创建一个备忘录信息,交给管理者处理
* @return
*/
public ConfigMemento saveMemento() {
return new ConfigMemento(configFile);
}
/**
* 获取之后并不直接返回,而是将备忘录的信息交给现在的配置文件
* @param memento
*/
public void getMemento(ConfigMemento memento) {
this.configFile = memento.getConfigFile();
}
}
Admin
package behavioral.memento;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class Admin {
private int cursorIdx = 0;
private List<ConfigMemento> mementoList = new ArrayList<>();
private Map<String, ConfigMemento> mementoMap = new ConcurrentHashMap<>();
public void append(ConfigMemento memento) {
mementoList.add(memento);
mementoMap.put(memento.getConfigFile().getVersionNo(), memento);
cursorIdx++;
}
public ConfigMemento undo() {
if (--cursorIdx <= 0) return mementoList.get(0);
return mementoList.get(cursorIdx);
}
public ConfigMemento redo() {
if (++cursorIdx > mementoList.size()) return mementoList.get(mementoList.size() - 1);
return mementoList.get(cursorIdx);
}
public ConfigMemento get(String versionNo) {
return mementoMap.get(versionNo);
}
}
测试
@Test
public void testMemento(){
Admin admin = new Admin();
ConfigOriginator configOriginator = new ConfigOriginator();
configOriginator.setConfigFile(new ConfigFile("1000001", "配置内容A=哈哈", new Date(), "花染梦"));
admin.append(configOriginator.saveMemento()); // 保存配置
configOriginator.setConfigFile(new ConfigFile("1000002", "配置内容A=嘻嘻", new Date(), "花染梦"));
admin.append(configOriginator.saveMemento()); // 保存配置
configOriginator.setConfigFile(new ConfigFile("1000003", "配置内容A=么么", new Date(), "花染梦"));
admin.append(configOriginator.saveMemento()); // 保存配置
configOriginator.setConfigFile(new ConfigFile("1000004", "配置内容A=嘿嘿", new Date(), "花染梦"));
admin.append(configOriginator.saveMemento()); // 保存配置
// 历史配置(回滚)
configOriginator.getMemento(admin.undo());
logger.info("历史配置(回滚)undo:{}", JSON.toJSONString(configOriginator.getConfigFile()));
// 历史配置(回滚)
configOriginator.getMemento(admin.undo());
logger.info("历史配置(回滚)undo:{}", JSON.toJSONString(configOriginator.getConfigFile()));
// 历史配置(前进)
configOriginator.getMemento(admin.redo());
logger.info("历史配置(前进)redo:{}", JSON.toJSONString(configOriginator.getConfigFile()));
// 历史配置(获取)
configOriginator.getMemento(admin.get("1000002"));
logger.info("历史配置(获取)get:{}", JSON.toJSONString(configOriginator.getConfigFile()));
}
/**
* 历史配置(回滚)undo:{"content":"配置内容A=嘿嘿","dateTime":1611384839507,"operator":"花染梦","versionNo":"1000004"}
* 历史配置(回滚)undo:{"content":"配置内容A=么么","dateTime":1611384839507,"operator":"花染梦","versionNo":"1000003"}
* 历史配置(前进)redo:{"content":"配置内容A=嘿嘿","dateTime":1611384839507,"operator":"花染梦","versionNo":"1000004"}
* 历史配置(获取)get:{"content":"配置内容A=嘻嘻","dateTime":1611384839507,"operator":"花染梦","versionNo":"1000002"}
*/
适用场景
-
当你需要创建对象状态快照来恢复其之前的状态时, 可以使用备忘录模式。
备忘录模式允许你复制对象中的全部状态 (包括私有成员变量), 并将其独立于对象进行保存。 尽管大部分人因为 “撤销” 这个用例才记得该模式, 但其实它在处理事务 (比如需要在出现错误时回滚一个操作) 的过程中也必不可少。
-
当直接访问对象的成员变量、 获取器或设置器将导致封装被突破时, 可以使用该模式。
备忘录让对象自行负责创建其状态的快照。 任何其他对象都不能读取快照, 这有效地保障了数据的安全性。
实现方式
-
确定担任原发器角色的类。 重要的是明确程序使用的一个原发器中心对象, 还是多个较小的对象。
-
创建备忘录类。 逐一声明对应每个原发器成员变量的备忘录成员变量。
-
将备忘录类设为不可变。 备忘录只能通过构造函数一次性接收数据。 该类中不能包含设置器。
-
如果你所使用的编程语言支持嵌套类, 则可将备忘录嵌套在原发器中; 如果不支持, 那么你可从备忘录类中抽取一个空接口, 然后让其他所有对象通过接口来引用备忘录。 你可在该接口中添加一些元数据操作, 但不能暴露原发器的状态。
-
在原发器中添加一个创建备忘录的方法。 原发器必须通过备忘录构造函数的一个或多个实际参数来将自身状态传递给备忘录。
该方法返回结果的类型必须是你在上一步中抽取的接口 (如果你已经抽取了)。 实际上,创建备忘录的方法必须直接与备忘录类进行交互。
-
在原发器类中添加一个用于恢复自身状态的方法。 该方法接受备忘录对象作为参数。 如果你在之前的步骤中抽取了接口, 那么可将接口作为参数的类型。 在这种情况下, 你需要将输入对象强制转换为备忘录, 因为原发器需要拥有对该对象的完全访问权限。
-
无论负责人是命令对象、 历史记录或其他完全不同的东西, 它都必须要知道何时向原发器请求新的备忘录、 如何存储备忘录以及何时使用特定备忘录来对原发器进行恢复。
-
负责人与原发器之间的连接可以移动到备忘录类中。 在本例中, 每个备忘录都必须与创建自己的原发器相连接。 恢复方法也可以移动到备忘录类中, 但只有当备忘录类嵌套在原发器中, 或者原发器类提供了足够多的设置器并可对其状态进行重写时, 这种方式才能实现。
备忘录模式优点
- 你可以在不破坏对象封装情况的前提下创建对象状态快照。
- 你可以通过让负责人维护原发器状态历史记录来简化原发器代码。
备忘录模式缺点
- 如果客户端过于频繁地创建备忘录, 程序将消耗大量内存。
- 负责人必须完整跟踪原发器的生命周期,这样才能销毁弃用的备忘录。
- 绝大部分动态编程语言 (例如 PHP、Python 和 JavaScript) 不能确保备忘录中的状态不被修改。