最近看一个网络主播玩了一款建造游戏,游戏主角可以采矿种植农作物建造房屋,并从npc手中购买各种武器工具蓝图牲畜来创建家园,甚至可以招募npc并培养其各种能力成为打手医生建筑师小偷等等。不是这款游戏吸引了我,而是这个主播的玩法吸引了我,纯粹耍流氓的玩法,见到npc卖好东西就偷,被发现了就耍流氓,直接拿着砍刀上去抢,但是抢有一定几率打不过,直接挂掉,所以每次偷东西之前他都会先存一下档,挂了在读档继续偷。其实这种存档的操作在电脑的好多软件中都存在,比如ctrl+z操作。
我们来想想这种操作如何用代码来实现,通用的实现方式就是在需要恢复操作的类中定义一个容器,容器中有一个信息结构来存储这个类在某个状态下的信息。然后这个类还需要提供存档和读档的函数接口。简单的情况下,这种操作或许可行,但是当备忘录管理操作过于复杂多变时,将备忘录的管理单独封装起来会是个更好的选择。这就是我们所说的备忘录模式。
备忘录模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在对象外部保存这个状态,这样就可以将该对象恢复到之前保存的状态。
一、特征
1、对象:即,这种模式是针对对象的,保存一个对象在不同情况下的状态,使这个对象的使用者可以根据需要随时恢复此对象到指定指定的状态下,这与创建型模式里面的原型模式有区别,原型模式关注的是如何快速的创建多个相同/相似的对象,然后使用者来使用这些对象,而备忘录模式关注的是如何方便的将一个对象恢复到某个状态。
2、不破坏封装性:即,备忘录模式的设计过程中不能破坏原类的封装性,这就要求这个对象的指定状态保存到外部的过程要封装到这个类内,外部不能访问这个类的私有状态。类似c++ 中的友元类可以处理此问题。
二、作用:
某些应用场景里,需要保存一个类在不同场景下的状态方便后面的恢复。这个时候就可以考虑用备忘录模式,也叫做存档模式。
三、实现:
Originator,具体需要做备忘的类,内部提供了备忘录创建和读取的函数,其创建一个备忘录丢给外部让其他人保存,自己该干嘛还干嘛,需要恢复指定状态的时候,使用备忘录读取函数读取一个备忘录并将备忘录中的状态信息读取到类内部。
Memento,备忘录,保存Originator某个场景下的状态,并封装状态读取接口,提供给Originator使用。
Caretaker,档案夹,保存所有对象的所有备忘录。
我们来看看如何实现那个拿砍刀偷东西的家园创建者的偷窃操作:
//Originator类
public class ZhangSan {
private String name;// 名字
private int position;// 地图位置
private int type;// 0,良民;1,小偷;2,抢劫犯;
private int Level;// 等级
public ZhangSan() {
this.name = "张三";
this.position = 2;
this.type = 0;
this.Level = 50;
}
public void steal() {
String strMoney = "RMB";
if (strMoney.length() != 0) {// 模拟偷窃被抓
type = 2;
}
}
void setMemento(Memento mem) {
// TODO Auto-generated method stub
this.name = mem.getInfo().getName();
this.position = mem.getInfo().getPos();
this.type = mem.getInfo().getType();
}
Memento createMemento() {
// TODO Auto-generated method stub
RoleData info = new RoleData();
info.setName(name);
info.setType(type);
info.setPos(position);
Memento mem = new Memento(info);
return mem;
}
void printStat()
{
System.out.printf("the role type:%d\n", type);
}
//Memento类
class Memento {
private RoleData my_info;
public Memento(RoleData info)
{
my_info = new RoleData();
my_info.setName(info.getName());
my_info.setPos(info.getPos());
my_info.setType(info.getType());
}
public RoleData getInfo()
{
RoleData tmp = new RoleData();
tmp.setName(my_info.getName());
tmp.setPos(my_info.getPos());
tmp.setType(my_info.getType());
return tmp;
}
}
class RoleData {
private String name;
private int type;
private int pos;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public int getPos() {
return pos;
}
public void setPos(int pos) {
this.pos = pos;
}
}
//CareTaker类
public class CareTaker {
Vector<Memento> memList = new Vector<Memento>();
void add(Memento mem)
{
memList.add(mem);
}
Memento get()
{
return memList.get(memList.size()-1);
}
}
应用场景:
public class Main {
public static void main(String[] args) {
// TODO Auto-generated method stub
CareTaker taker = new CareTaker();
ZhangSan role = new ZhangSan();
taker.add(role.createMemento());
role.printStat();
role.steal();
role.printStat();
role.setMemento(taker.get());
role.printStat();
}
}
昨天(20190619)一个同事问我为什么要用备忘录模式,我回答他:解耦啊。结果我被锤了。
我说的没错啊,他继续问,数据阶段性存档备忘这很常见,但是为什么不在备忘发起者(Originator)中直接创建一个容器来存取备忘录,这样不用担心发起者数据的封装性问题,也不需要维护备忘录和档案夹,多好啊,我哑口无言。。。。
我们来看看百度百科上所说的归档模式的优缺点:
一、备忘录模式的优点
1、有时一些发起人对象的内部信息必须保存在发起人对象以外的地方,但是必须要由发起人对象自己读取,这时,使用备忘录模式可以把复杂的发起人内部信息对其他的对象屏蔽起来,从而可以恰当地保持封装的边界。
2、本模式简化了发起人类。发起人不再需要管理和保存其内部状态的一个个版本,客户端可以自行管理他们所需
要的这些状态的版本。
3、当发起人角色的状态改变的时候,有可能这个状态无效,这时候就可以使用暂时存储起来的备忘录将状态复原。
二、备忘录模式的缺点:
1、如果发起人角色的状态需要完整地存储到备忘录对象中,那么在资源消耗上面备忘录对象会很昂贵。
2、当负责人角色将一个备忘录 存储起来的时候,负责人可能并不知道这个状态会占用多大的存储空间,从而无法提醒用户一个操作是否很昂贵。
3、当发起人角色的状态改变的时候,有可能这个协议无效。如果状态改变的成功率不高的话,不如采取“假如”协议模式。
以下是我个人对三个优点的理解:
1、备忘录模式针对的是数据,而不是类。有时候,数据的处理和数据本身是分开的,发起者会对这些数据进行处理使用,但是他并没有权限对这些数据进行内部封装保存,原因很多,可能这个数据也提供给其他类使用。这时候,你强硬的在发起者内部做备忘录就会缓存这些数据,本来很清晰的层次结构会被打破,增加了数据处理类和数据间的耦合,并且如果处理这个数据的很多类都要做备忘录怎么办?每个类里面都缓存数据吗?所以我个人理解备忘录是备忘的数据状态,而这个状态却不一定就在某个类里。
2、备忘录模式封装的是备忘录的管理操作。如果备忘的状态数据就由发起者自己内部管理,那么就可以由发起者自己做备忘录的管理了吗?同样不是。如果备忘录的管理相当简单,那么我们会让发起者自己管理备忘录,但是如果备忘录的管理很复杂呢?比如备忘录本身很复杂,保存的状态数据很复杂,又比如备忘录的骚操作很多,我需要对备忘录进行定时删除,定数量删除,定ID删除,定ID索引,还有各种增加,修改操作,那么如此庞大的备忘录管理维护操作再放在发起者类里就会严重的影响其封装,单一职责原则也不允许,那么将备忘录的管理操作单独提出来做封装就是更好的选择了。
3、这个就不用多说了,这个就是备忘录的目的,可以复原状态。