设计模式之【备忘录模式】,“后悔药”是可以有的
文章目录
一、什么是备忘录模式
备忘录模式(Memento Pattern)又称为快照模式(Snapshot Pattern)或令牌模式(Token Pattern),是指在不破坏封装的前提下,捕获一个对象的内部状态,并在对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态,属于行为型模式。
在软件系统中,备忘录模式可以为我们提供一种“后悔药”的机制,它通过存储系统各个历史状态的快照,使得我们可以在任一时刻将系统回滚到某一个历史状态。
备忘录模式本质是从发起人实体类(Originator)隔离存储功能,降低实体类的职责。同时由于存储信息(Memento)独立,且存储信息的实体交由管理类(Caretaker)管理,则可以通过为管理类扩展额外的功能对存储信息进行扩展操作(比如增加历史快照功能)。
1、备忘录模式使用场景
- 需要保存历史快照的场景。
- 希望在对象之外保存状态,且除了自己其他类对象无法访问状态保存具体内容。
比如,玩游戏时的中间结果的存档功能、如 Word、记事本、Photoshop,idea等软件在编辑时按Ctrl+Z 组合键,还有数据库中事务操作。
2、备忘录模式优缺点
优点:
- 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
- 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
- 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。
缺点:
- 资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。
- 如果需要保存的状态过多时,每一次保存都会消耗很多内存。
3、备忘录模式的三大角色
备忘录模式的主要角色如下:
- 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
- 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人,且防止发起人以外的对象访问。
- 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。
4、白箱备忘录和黑箱备忘录
备忘录有两个等效的接口:
- 窄接口:管理者(Caretaker)对象(和其他发起人对象之外的任何对象)看到的是备忘录的窄接口(narror Interface),这个窄接口只允许他把备忘录对象传给其他的对象。
- 宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口(wide Interface),这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态。
白箱备忘录使用的就是宽接口,白箱备忘录模式是破坏封装性的。但是通过程序员自律,同样可以在一定程度上实现模式的大部分用意。
黑箱备忘录使用的是窄接口,将备忘录角色封装在发起人角色的内部形成一个私有的内部类,并实现窄接口。管理者只管理窄接口,这样可以屏蔽备忘录角色的细节。
5、思考:备份频率快,备份对象大的备忘录应该如何设计
假设每当有数据改动,我们都需要生成一个备份,以备之后恢复。如果需要备份的数据很大,这样高频率的备份,不管是对存储(内存或者硬盘)的消耗,还是对时间的消耗,都可能是无法接受的。想要解决这个问题,我们一般会采用“低频率全量备份”和“高频率增量备份”相结合的方法。
当我们需要恢复到某一时间点的备份的时候,如果这一时间点有做全量备份,我们直接拿来恢复就可以了。如果这一时间点没有对应的全量备份,我们就先找到最近的一次全量备份,然后用它来恢复,之后执行此次全量备份跟这一时间点之间的所有增量备份,也就是对应的操作或者数据变动。这样就能减少全量备份的数量和频率,减少对时间、内存的消耗。
其实很多设计原则和设计思想都是互通的,mysql的备份与恢复、redis的备份与恢复都是参考了这种实现原理。
二、实例
1、备忘录模式的一般写法
// 发起人角色
public class Originator {// 内部状态private String state;public String getState() {return this.state;}public void setState(String state) {this.state = state;}// 创建一个备忘录public Memento createMemento() {return new Memento(this.state);}// 从备忘录恢复public void restoreMemento(Memento memento) {this.setState(memento.getState());}
}
// 备忘录角色
public class Memento {private String state;public Memento(String state){this.state = state;}public String getState() {return this.state;}public void setState(String state) {this.state = state;}
}
// 管理者角色
public class Caretaker {// 备忘录对象private Memento memento;public Memento getMemento() {return this.memento;}public void storeMemento(Memento memento) {this.memento = memento;}}
// 测试类
public class Test {public static void main(String[] args) {//来一个发起人Originator originator = new Originator();//来一个备忘录管理员Caretaker caretaker = new Caretaker();//管理员存储发起人的备忘录caretaker.storeMemento(originator.createMemento());//发起人从管理员获取备忘录进行回滚originator.restoreMemento(caretaker.getMemento());}
}
2、使用栈管理富文本编辑器
我们使用富文本编辑器时,会经常写入、撤销、修改。因此我们需要将每一时刻的修改记录都要保存在草稿箱中。
// 发起人角色编辑器
public class Editor {private String title;private String content;private String imgs;public Editor(String title, String content, String imgs) {this.title = title;this.content = content;this.imgs = imgs;}public String getTitle() {return title;}public String getContent() {return content;}public String getImgs() {return imgs;}public void setTitle(String title) {this.title = title;}public void setContent(String content) {this.content = content;}public void setImgs(String imgs) {this.imgs = imgs;}public ArticleMemento saveToMemento(){ArticleMemento articleMemento = new ArticleMemento(this.title,this.content,this.imgs);return articleMemento;}public void undoFromMemento(ArticleMemento articleMemento){this.title = articleMemento.getTitle();this.content = articleMemento.getContent();this.imgs = articleMemento.getImgs();}@Overridepublic String toString() {return "Editor{" +"title='" + title + '\'' +", content='" + content + '\'' +", imgs='" + imgs + '\'' +'}';}
}
// 备忘录角色
public class ArticleMemento {private String title;private String content;private String imgs;public ArticleMemento(String title, String content, String imgs) {this.title = title;this.content = content;this.imgs = imgs;}public String getTitle() {return title;}public String getContent() {return content;}public String getImgs() {return imgs;}@Overridepublic String toString() {return "ArticleMemento{" +"title='" + title + '\'' +", content='" + content + '\'' +", imgs='" + imgs + '\'' +'}';}
}
// 管理角色 草稿箱
public class DraftsBox {private final Stack<ArticleMemento> STACK = new Stack<ArticleMemento>();public ArticleMemento getMemento(){ArticleMemento articleMemento = STACK.pop();return articleMemento;}public void addMemento(ArticleMemento articleMemento){STACK.push(articleMemento);}}
草稿箱中定义的Stack类是Vector的一个子类,它实现了一个标准的后进先出的栈。主要定义了以下方法:
方法定义 | 方法描述 |
---|---|
boolean empty() | 测试栈是否为空 |
Object peek() | 查看栈顶对象,但不从栈中移除它 |
Object pop() | 移除栈顶对象,并作为此函数的返回值 |
Object push(Object element) | 把对象压入栈顶 |
int search(Object element) | 返回对象在栈中的位置,以1为基数 |
// 测试类
public class Test {public static void main(String[] args) {DraftsBox draftsBox = new DraftsBox();Editor editor = new Editor("标题1","内容1","图片1");ArticleMemento articleMemento = editor.saveToMemento();draftsBox.addMemento(articleMemento);System.out.println("标题:" + editor.getTitle() + "\n" +"内容:" + editor.getContent() + "\n" +"插图:" + editor.getImgs() + "\n暂存成功");System.out.println("完整的信息" + editor);System.out.println("==========首次修改文章===========");editor.setTitle("标题2");editor.setContent("内容2");editor.setImgs("图片2");System.out.println("==========首次修改文章完成===========");System.out.println("完整的信息" + editor);articleMemento = editor.saveToMemento();draftsBox.addMemento(articleMemento);System.out.println("==========保存到草稿箱===========");System.out.println("==========第2次修改文章===========");editor.setTitle("标题3");editor.setContent("内容3");editor.setImgs("图片3");System.out.println("完整的信息" + editor);System.out.println("==========第2次修改文章完成===========");System.out.println("==========第1次撤销===========");articleMemento = draftsBox.getMemento();editor.undoFromMemento(articleMemento);System.out.println("完整的信息" + editor);System.out.println("==========第1次撤销完成===========");System.out.println("==========第2次撤销===========");articleMemento = draftsBox.getMemento();editor.undoFromMemento(articleMemento);System.out.println("完整的信息" + editor);System.out.println("==========第2次撤销完成===========");}
}
执行结果:
3、游戏状态恢复案例
游戏中的某个场景,一游戏角色有生命力、攻击力、防御力等数据,在打Boss前和后一定会不一样的,我们允许玩家如果感觉与Boss决斗的效果不理想可以让游戏恢复到决斗之前的状态。
(1)“白箱”备忘录模式
备忘录角色对任何对象都提供一个接口,即宽接口,备忘录角色的内部所存储的状态就对所有对象公开。类图如下:
//游戏角色类
public class GameRole {private int vit; //生命力private int atk; //攻击力private int def; //防御力//初始化状态public void initState() {this.vit = 100;this.atk = 100;this.def = 100;}//战斗public void fight() {this.vit = 0;this.atk = 0;this.def = 0;}//保存角色状态public RoleStateMemento saveState() {return new RoleStateMemento(vit, atk, def);}//回复角色状态public void recoverState(RoleStateMemento roleStateMemento) {this.vit = roleStateMemento.getVit();this.atk = roleStateMemento.getAtk();this.def = roleStateMemento.getDef();}public void stateDisplay() {System.out.println("角色生命力:" + vit);System.out.println("角色攻击力:" + atk);System.out.println("角色防御力:" + def);}public int getVit() {return vit;}public void setVit(int vit) {this.vit = vit;}public int getAtk() {return atk;}public void setAtk(int atk) {this.atk = atk;}public int getDef() {return def;}public void setDef(int def) {this.def = def;}
}
//游戏状态存储类(备忘录类)
public class RoleStateMemento {private int vit;private int atk;private int def;public RoleStateMemento(int vit, int atk, int def) {this.vit = vit;this.atk = atk;this.def = def;}public int getVit() {return vit;}public void setVit(int vit) {this.vit = vit;}public int getAtk() {return atk;}public void setAtk(int atk) {this.atk = atk;}public int getDef() {return def;}public void setDef(int def) {this.def = def;}
}
//角色状态管理者类
public class RoleStateCaretaker {private RoleStateMemento roleStateMemento;public RoleStateMemento getRoleStateMemento() {return roleStateMemento;}public void setRoleStateMemento(RoleStateMemento roleStateMemento) {this.roleStateMemento = roleStateMemento;}
}
//测试类
public class Client {public static void main(String[] args) {System.out.println("------------大战Boss前------------");//大战Boss前GameRole gameRole = new GameRole();gameRole.initState();gameRole.stateDisplay();//保存进度RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();roleStateCaretaker.setRoleStateMemento(gameRole.saveState());System.out.println("------------大战Boss后------------");//大战Boss时,损耗严重gameRole.fight();gameRole.stateDisplay();System.out.println("------------恢复之前状态------------");//恢复之前状态gameRole.recoverState(roleStateCaretaker.getRoleStateMemento());gameRole.stateDisplay();}
}
分析:白箱备忘录模式是破坏封装性的。但是通过程序员自律,同样可以在一定程度上实现模式的大部分用意
。
(2)“黑箱”备忘录模式
备忘录角色对发起人对象提供一个宽接口,而为其他对象提供一个窄接口。在Java语言中,实现双重接口的办法就是将备忘录类设计成发起人类的内部成员类。
将 RoleStateMemento 设为 GameRole 的内部类,从而将 RoleStateMemento 对象封装在GameRole 里面;在外面提供一个标识接口 Memento 给 RoleStateCaretaker 及其他对象使用。
这样 GameRole 类看到的是 RoleStateMemento 所有的接口,而 RoleStateCaretaker 及其他对象看到的仅仅是标识接口 Memento 所暴露出来的接口,从而维护了封装型。类图如下:
// 窄接口 Memento ,这是一个标识接口,因此没有定义出任何的方法
public interface Memento {
}
// 定义发起人类 GameRole ,并在内部定义备忘录内部类 RoleStateMemento (该内部类设置为私有的)
//游戏角色类
public class GameRole {private int vit; //生命力private int atk; //攻击力private int def; //防御力//初始化状态public void initState() {this.vit = 100;this.atk = 100;this.def = 100;}//战斗public void fight() {this.vit = 0;this.atk = 0;this.def = 0;}//保存角色状态public Memento saveState() {return new RoleStateMemento(vit, atk, def);}//回复角色状态public void recoverState(Memento memento) {RoleStateMemento roleStateMemento = (RoleStateMemento) memento;this.vit = roleStateMemento.getVit();this.atk = roleStateMemento.getAtk();this.def = roleStateMemento.getDef();}public void stateDisplay() {System.out.println("角色生命力:" + vit);System.out.println("角色攻击力:" + atk);System.out.println("角色防御力:" + def);}public int getVit() {return vit;}public void setVit(int vit) {this.vit = vit;}public int getAtk() {return atk;}public void setAtk(int atk) {this.atk = atk;}public int getDef() {return def;}public void setDef(int def) {this.def = def;}// 备忘录角色内部类private class RoleStateMemento implements Memento {private int vit;private int atk;private int def;public RoleStateMemento(int vit, int atk, int def) {this.vit = vit;this.atk = atk;this.def = def;}public int getVit() {return vit;}public void setVit(int vit) {this.vit = vit;}public int getAtk() {return atk;}public void setAtk(int atk) {this.atk = atk;}public int getDef() {return def;}public void setDef(int def) {this.def = def;}}
}
负责人角色类 RoleStateCaretaker 能够得到的备忘录对象是以 Memento 为接口的,由于这个接口仅仅是一个标识接口,因此负责人角色不可能改变这个备忘录对象的内容
//角色状态管理者类
public class RoleStateCaretaker {private Memento memento;public Memento getMemento() {return memento;}public void setMemento(Memento memento) {this.memento = memento;}
}
// 测试类
public class Client {public static void main(String[] args) {System.out.println("------------大战Boss前------------");//大战Boss前GameRole gameRole = new GameRole();gameRole.initState();gameRole.stateDisplay();//保存进度RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();roleStateCaretaker.setMemento(gameRole.saveState());System.out.println("------------大战Boss后------------");//大战Boss时,损耗严重gameRole.fight();gameRole.stateDisplay();System.out.println("------------恢复之前状态------------");//恢复之前状态gameRole.recoverState(roleStateCaretaker.getMemento());gameRole.stateDisplay();}
}