备忘录模式
# 定义
忘录模式是以可以恢复或者说回滚,配置、版本、悔棋为核心功能的设计模式,而这种设计模式属于行为模式。在功能实现上是以不破坏原对象为基础增加备忘录操作类,记录原对象的行为从而实现备忘录模式。
>备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。
优点:
1. 给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。
2. 实现了信息的封装,使得用户不需要关心状态的保存细节。
缺点:消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。
使用场景:
1. 需要保存/恢复数据的相关状态场景。
2. 提供一个可回滚的操作。
# 实践
本次模拟系统在发布上线的过程中记录线上配置文件用于紧急回滚
代码结构:

备忘录模式模型结构:

- 以上是工程结构的一个类图,其实相对来说并不复杂,除了原有的配置类(ConfigFile)以外,只新增加了三个类。
- ConfigMemento:备忘录类,相当于是对原有配置类的扩展
- ConfigOriginator:记录者类,获取和返回备忘录类对象信息
- Admin:管理员类,用于操作记录备忘信息,比如你一些列的顺序执行了什么或者某个版本下的内容信息
配置类 ConfigFile.java
```java
@Data
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;
}
}
```
备忘录类 ConfigMemento.java
```java
@Data
public class ConfigMemento {
private ConfigFile configFile;
public ConfigMemento(ConfigFile configFile) {
this.configFile = configFile;
}
}
```
记录者类 ConfigOriginator.java
```java
@Data
public class ConfigOriginator {
private ConfigFile configFile;
public ConfigMemento saveMemento(){
return new ConfigMemento(configFile);
}
public void getMemento(ConfigMemento memento){
this.configFile = memento.getConfigFile();
}
}
```
- 记录者类除了对ConfigFile配置类增加了获取和设置方法外,还增加了保存 saveMemento()、获取 getMemento(ConfigMemento memento)。
- saveMemento:保存备忘录的时候会创建一个备忘录信息,并返回回去,交给管理者处理。
- getMemento:获取的之后并不是直接返回,而是把备忘录的信息交给现在的配置文件 this.configFile,这部分需要注意。
管理员类 Admin.java
```java
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);
}
}
```
- 在这个类中主要实现的核心功能就是记录配置文件信息,也就是备忘录的效果,之后提供可以回滚和获取的方法,拿到备忘录的具体内容。
- 同时这里设置了两个数据结构来存放备忘录,实际使用中可以按需设置。List<ConfigMemento>、Map<String, ConfigMemento>。
- 最后是提供的备忘录操作方法;存放(append)、回滚(undo)、返回(redo)、定向获取(get),这样四个操作方法。
测试类
```java
@Slf4j
public class ApiTest {
@Test
public void test() {
Admin admin = new Admin();
ConfigOriginator configOriginator = new ConfigOriginator();
configOriginator.setConfigFile(new ConfigFile("1000001", "配置内容A=哈哈", new Date(), "lixj"));
admin.append(configOriginator.saveMemento()); // 保存配置
configOriginator.setConfigFile(new ConfigFile("1000002", "配置内容A=嘻嘻", new Date(), "lixj"));
admin.append(configOriginator.saveMemento()); // 保存配置
configOriginator.setConfigFile(new ConfigFile("1000003", "配置内容A=么么", new Date(), "lixj"));
admin.append(configOriginator.saveMemento()); // 保存配置
configOriginator.setConfigFile(new ConfigFile("1000004", "配置内容A=嘿嘿", new Date(), "lixj"));
admin.append(configOriginator.saveMemento()); // 保存配置
// 历史配置(回滚)
configOriginator.getMemento(admin.undo());
log.info("历史配置(回滚)undo:{}", JSON.toJSONString(configOriginator.getConfigFile()));
// 历史配置(回滚)
configOriginator.getMemento(admin.undo());
log.info("历史配置(回滚)undo:{}", JSON.toJSONString(configOriginator.getConfigFile()));
// 历史配置(前进)
configOriginator.getMemento(admin.redo());
log.info("历史配置(前进)redo:{}", JSON.toJSONString(configOriginator.getConfigFile()));
// 历史配置(获取)
configOriginator.getMemento(admin.get("1000001"));
log.info("历史配置(获取)get:{}", JSON.toJSONString(configOriginator.getConfigFile()));
}
}
```
测试结果:
```
15:28:53.912 [main] INFO ApiTest - 历史配置(回滚)undo:{"content":"配置内容A=嘿嘿","dateTime":1645514933850,"operator":"lixj","versionNo":"1000004"}
15:28:53.914 [main] INFO ApiTest - 历史配置(回滚)undo:{"content":"配置内容A=么么","dateTime":1645514933850,"operator":"lixj","versionNo":"1000003"}
15:28:53.914 [main] INFO ApiTest - 历史配置(前进)redo:{"content":"配置内容A=嘿嘿","dateTime":1645514933850,"operator":"lixj","versionNo":"1000004"}
15:28:53.914 [main] INFO ApiTest - 历史配置(获取)get:{"content":"配置内容A=哈哈","dateTime":1645514933850,"operator":"lixj","versionNo":"1000001"}
Process finished with exit code 0
```
- 从测试效果上可以看到,历史配置按照我们的指令进行了回滚和前进,以及最终通过指定的版本进行获取,符合预期结果。
# 总结
- 此种设计模式的方式可以满足在不破坏原有属性类的基础上,扩充了备忘录的功能。虽然和我们平时使用的思路是一样的,但在具体实现上还可以细细品味,这样的方式在一些源码中也有所体现。
- 在以上的实现中我们是将配置模拟存放到内存中,如果关机了会导致配置信息丢失,因为在一些真实的场景里还是需要存放到数据库中。那么此种存放到内存中进行回复的场景也不是没有,比如;Photoshop、运营人员操作ERP配置活动,那么也就是即时性的一般不需要存放到库中进行恢复。另外如果是使用内存方式存放备忘录,需要考虑存储问题,避免造成内存大量消耗。
- 设计模式的学习都是为了更好的写出可扩展、可管理、易维护的代码,而这个学习的过程需要自己不断的尝试实际操作,理论的知识与实际结合还有很长一段距离。切记多多上手!
>原文链接:https://github.com/fuzhengwei/CodeGuide/blob/master/docs/md/develop/design-pattern/2020-06-28-%E9%87%8D%E5%AD%A6%20Java%20%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E3%80%8A%E5%AE%9E%E6%88%98%E5%A4%87%E5%BF%98%E5%BD%95%E6%A8%A1%E5%BC%8F%E3%80%8B.md

【设计模式】备忘录模式