名称
备忘录(MEMENTO),别名:Token
目的
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。 这样以后就可将该对象恢复到原先保存的状态。
适用性
在以下情况下使用备忘录模式:
必须保存一个对象在某一个时刻的(部分)状态,这样以后需要时它才能恢复到先前的状态。
如果一个用接口来让其它对象直接得到这些状态,将会暴露对象的实现细节并破坏对象的封装性。
结构
备忘录(Memento):
备忘录存储原发器对象的内部状态。原发器根据需要决定备忘录存储原发器的哪些内部状态。
防止原发器以外的其他对象访问备忘录。备忘录实际上有两个接口,管理者(caretaker)只能看到备忘录的窄接口(它只能将备忘录传递给其他对象)。相反,原发器能够看到一个宽接口,允许它访问返回到先前状态所需的所有数据。理想情况是只允许生成本备忘录的那个原发器访问本备忘录的内部状态。
负责人(Caretaker):
负责保存好备忘录。
不能对备忘录的内容进行操作或检查。
原发器(Originator):
原发器创建一个备忘录,用以记录当前时刻它的内部状态。
使用备忘录恢复内部状态.。
协作
管理器向原发器请求一个备忘录,保留一段时间后,将其送回给原发器,如下面的交互图所示。有时管理者不会将备忘录返回给原发器,因为原发器可能根本不需要退到先前的状态。
备忘录是被动的。只有创建备忘录的原发器会对它的状态进行赋值和检索。
交互图如下:
效果
优点
保持封装边界。使用备忘录可以避免暴露一些只应由原发器管理却又必须存储在原发器之外的信息。该模式把可能很复杂的Originator内部信息对其他对象屏蔽起来,从而保持了封装边界。
简化了原发器。在其他的保持封装性的设计中,Originator负责保持客户请求过的内部状态版本,这就把所有存储管理的重任交给了Originator(符合单一职责原则)。让客户管理它们请求的状态将会简化Originator,并且使得客户工作结束时无需通知原发器。
提供恢复机制。备忘录模式提供了一种可以恢复状态的机制,使得用户可以在需要的时候方便地将数据恢复到某个历史状态。
封装内部状态。备忘录模式实现了内部状态的封装,除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
缺点
使用备忘录可能代价很高。如果原发器在生成备忘录时必须拷贝并存储大量的信息,或者客户非常频繁地创建备忘录和恢复原发器状态,可能会导致非常大的开销。除非封装和恢复Originator状态的开销不大,否则该模式可能并不合适。
定义窄接口和宽接口。在一些语言中可能难以保证只有原发器可访问备忘录的状态。
维护备忘录的潜在代价。管理器负责删除它所维护的备忘录。然而,管理器不知道备忘录中有多少个状态。因此当存储备忘录时,一个本来很小的管理器,可能会产生大量的存储开销。
实现
语言支持。备忘录有两个接口:一个为原发器所使用的宽接口,一个为其他对象所使用的窄接口。理想的实现语言应可支持两级的静态保护。
存储增量式改变。如果备忘录的创建及其返回(给它们的原发器)的顺序是可预测的,备忘录可以仅存储原发器内部状态的增量改变。
应用
问题
当我们在玩通关游戏的时候,往往不会一次成功,通常会失败很多次,那么在游戏设计的时候通常会有很多存档点,当玩家game over的时候可以通过最近的存档点读取到当时玩家的状态并进行恢复,这里的存档点就相当于我们的备忘录。设计一款游戏存档系统,假设角色Role的状态有血条和蓝条,那么就要提供保存存档(saveArchive)和恢复存档(restoreArchive)的方法,这里的角色就是原发器;存档(Archive,备忘录)的设计很简单,只需要保存一份血条和蓝条就行了,并提供它们的get/set方法;存档管理器(ArchiveManager,负责人)保持一个存档的引用就行(如果要有多个存档点就用集合存储),并提供get/set方法。至此,一款游戏存档系统设计完成。
示例
UML
代码示例
备忘录:存档
package com.ysj.part5.memento.memento;
import lombok.Data;
/**
* 备忘录:存档
*/
public class Archive {
/**
* 血条
*/
private int bloodFlow;
/**
* 蓝条
*/
private int magicPoint;
public Archive(int bloodFlow, int magicPoint) {
this.bloodFlow = bloodFlow;
this.magicPoint = magicPoint;
}
public int getBloodFlow() {
return bloodFlow;
}
public int getMagicPoint() {
return magicPoint;
}
}
负责人:存档管理器
package com.ysj.part5.memento.caretaker;
import com.ysj.part5.memento.memento.Archive;
import lombok.Data;
/**
* 负责人:存档管理器
*/
public class ArchiveManager {
/**
* 存档
*/
private Archive archive;
public Archive getMemento() {
return archive;
}
public void setMemento(Archive archive) {
this.archive = archive;
}
}
原发器:游戏角色
package com.ysj.part5.memento.originator;
import com.ysj.part5.memento.memento.Archive;
/**
* 原发器:游戏角色
*/
public class Role {
/**
* 血条
*/
private int bloodFlow;
/**
* 蓝条
*/
private int magicPoint;
public Role(int bloodFlow, int magicPoint) {
this.bloodFlow = bloodFlow;
this.magicPoint = magicPoint;
}
public int getBloodFlow() {
return bloodFlow;
}
public void setBloodFlow(int bloodFlow) {
this.bloodFlow = bloodFlow;
}
public int getMagicPoint() {
return magicPoint;
}
public void setMagicPoint(int magicPoint) {
this.magicPoint = magicPoint;
}
/**
* @return void
* @desc 展示角色当前状态
*/
public void display() {
System.out.println("用户当前状态:");
System.out.println("血量:" + getBloodFlow() + ";蓝量:" + getMagicPoint());
}
/**
* @return
* @return Memento
* @desc 保持存档、当前状态
*/
public Archive saveArchive() {
return new Archive(getBloodFlow(), getMagicPoint());
}
/**
* @param archive
* @return void
* @desc 恢复存档
*/
public void restoreArchive(Archive archive) {
this.bloodFlow = archive.getBloodFlow();
this.magicPoint = archive.getMagicPoint();
}
}
客户端:游戏
package com.ysj.part5.memento;
import cn.hutool.core.thread.ThreadUtil;
import com.ysj.part5.memento.caretaker.ArchiveManager;
import com.ysj.part5.memento.originator.Role;
import java.util.Scanner;
/**
* 备忘录模式
* 在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
*/
public class Client {
public static void main(String[] args) {
//打BOSS之前:血、蓝全部满值
Role role = new Role(100, 100);
System.out.println("----------大战BOSS之前----------");
role.display();
ThreadUtil.sleep(2000);
// 保存状态
ArchiveManager archiveManager = new ArchiveManager();
archiveManager.setMemento(role.saveArchive());
//大战BOSS,快come over了
role.setBloodFlow(20);
role.setMagicPoint(20);
System.out.println("----------大战BOSS----------");
role.display();
ThreadUtil.sleep(2000);
//大战BOSS,已经game over了
role.setBloodFlow(0);
role.setMagicPoint(10);
System.out.println("----------大战BOSS,挑战失败----------");
role.display();
ThreadUtil.sleep(2000);
// 询问问价是否读档
System.out.println("----------是否读取存档(yes or no)----------");
Scanner scanner = new Scanner(System.in);
String answer = scanner.nextLine();
if (answer.equals("yes")){
// 模拟恢复过程
System.out.print("*");
ThreadUtil.sleep(500);
System.out.print("**");
ThreadUtil.sleep(500);
System.out.print("***");
ThreadUtil.sleep(500);
System.out.print("****");
ThreadUtil.sleep(500);
System.out.print("*****");
ThreadUtil.sleep(500);
System.out.print("******");
ThreadUtil.sleep(500);
System.out.print("******");
ThreadUtil.sleep(500);
System.out.print("******");
ThreadUtil.sleep(500);
System.out.println("**");
ThreadUtil.sleep(1000);
//恢复存档
role.restoreArchive(archiveManager.getMemento());
System.out.println("----------恢复----------");
role.display();
}else {
System.out.println("----------游戏结束----------");
}
}
}
已知应用
Java Swing
JTextComponent:在Java Swing中,JTextComponent 类提供了一个 UndoManager,用于实现文本组件的撤销功能。UndoManager 实际上就是一个备忘录模式的应用。
JEditorPane:JEditorPane 和其他文本编辑组件也使用了备忘录模式来实现撤销和重做功能。
Spring Framework
Transaction Management:Spring 的事务管理模块使用了备忘录模式来记录事务的状态,以便在事务失败时能够回滚到之前的状态。
Microsoft Word
撤销功能:Word 文档编辑器中的撤销功能也是备忘录模式的一个典型应用。每次编辑操作都会保存一个备忘录,以便用户可以撤销或重做。
评论区