文章目录

  • 一、什么是备忘录模式
  • 1、备忘录模式使用场景
  • 2、备忘录模式优缺点
  • 3、备忘录模式的三大角色
  • 4、白箱备忘录和黑箱备忘录
  • 5、思考:备份频率快,备份对象大的备忘录应该如何设计
  • 二、实例
  • 1、备忘录模式的一般写法
  • 2、使用栈管理富文本编辑器
  • 3、游戏状态恢复案例
  • (1)“白箱”备忘录模式
  • (2)“黑箱”备忘录模式


一、什么是备忘录模式

备忘录模式(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();
    }

    @Override
    public 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;
    }

    @Override
    public 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次撤销完成===========");

    }
}

执行结果:

设计模式之【备忘录模式】,“后悔药”是可以有的_数据_02

3、游戏状态恢复案例

游戏中的某个场景,一游戏角色有生命力、攻击力、防御力等数据,在打Boss前和后一定会不一样的,我们允许玩家如果感觉与Boss决斗的效果不理想可以让游戏恢复到决斗之前的状态。

(1)“白箱”备忘录模式

备忘录角色对任何对象都提供一个接口,即宽接口,备忘录角色的内部所存储的状态就对所有对象公开。类图如下:

设计模式之【备忘录模式】,“后悔药”是可以有的_System_03

//游戏角色类
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 所暴露出来的接口,从而维护了封装型。类图如下:

设计模式之【备忘录模式】,“后悔药”是可以有的_数据_04

// 窄接口 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();
	}
}