设计模式_备忘录模式_Memento

案例引入

游戏角色有攻击力和防御力,在大战Boss前保存自身的状态(攻击力和防御力),当大战Boss后攻击力和防御力下降,可以从备忘录对象恢复到大战前的状态

传统设计方案

针对每一种角色,设计一个类来存储该角色的状态

【分析】

  • 一个对象,就对应一个保存对象状态的对象, 这样当我们游戏的对象很多时,不利于管理,开销也很大
  • 传统的方式是简单地做备份,new出另外一个对象出来,再把需要备份的数据放到这个新对象,但是这样暴露了对象内部的细节
  • 优化方式:使用备忘录模式

介绍

基本介绍

  • 备忘录模式在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态
  • 可以这样理解备忘录模式: 现实生活中的备忘录是用来记录某些要去做的事情或者是记录已经达成的共同意见的事情,以防忘记。而在软件层面,备忘录对象主要用来记录一个对象的某种状态,或者某些数据,当要做回退时,可以从备忘录对象里获取原来的数据进行恢复操作
  • 备忘录模式属于行为型模式

登场角色

  • Originator(生成者):Originator角色会在保存自己的最新状态时生成Memento角色。当把以前保存的Memento角色传递给Originator角色时,它会将自已恢复至生成该Memento角色时的状态
  • Memento(备忘录):Memento角色会将Originator 角色的内部信息整合在一起。在 Memento 角色中虽然保存了Originator 角色的信息,但它不会向外部公开这些信息(通过对方法的权限字符进行设置)
  • Caretaker(负责人):当Caretaker角色想要保存当前的Originator角色的状态时,会通知Originator角色Originator角色在接收到通知后会生成Memento角色的实例并将其返回给Caretaker角色。由于以后可能会用Memento实例来将Originator恢复至原来的状态,因此Caretaker角色会一直保存 Memento实例在示例程序中。Caretaker角色只能使用Memento角色的两种接口(API)中的窄接口,也就是说它无法访问Memento角色内部的所有信息(比如案例三中Caretaker角色只能获取Memento角色的金钱信息,无法进行创建Memento实例等操作)。它只是将Originator角色生成的Memento角色当作一个黑盒子保存起来。虽然Originator角色Memento角色之间是强关联关系,但Caretaker角色Memento角色之间是弱关联关系。Memento角色Caretaker角色隐藏了自身的内部信息

【划分Caretaker角色Originator角色的意义】

Caretaker角色的职责是决定何时拍摄快照,何时撤销以及保存Memento角色。另一方面,Originator 角色的职责则是生成Memento角色和使用接收到的Memento角色来恢复自己的状态。有了这样的职责分担,当我们需要应对以下的需求变更时,就不用修改Originator角色

  • 变更为可以多次撤销
  • 变更为不仅可以撤销,还可以将现在的状态保存在文件中

【Caretaker角色只能通过窄接口(API)来操作 Memento角色,如果Caretaker角色可以随意地操作Memento角色,会发生什么问题呢】

  • 破坏对象的封装性:Memento角色负责存储Originator角色的状态数据,如果Caretaker角色可以随意地操作Memento角色,那么就破坏了Memento对象和Originator对象的封装性
  • 不安全的状态恢复:如果Caretaker角色可以随意地操作Memento角色,可能会改变Memento对象中存储的状态数据,导致状态恢复产生错误
  • 无法保证数据的完整性:如果Caretaker角色可以随意地操作Memento角色,可能会在不正确的时间点保存状态数据,导致数据的不完整性

案例实现

案例一(基础案例)

类图
  • Originator:需要被保存状态的对象
  • Memento:备忘录对象,负责记录Originator对象的状态
  • Caretaker:守护者对象,负责保存多个备忘录对象,一般使用集合进行管理,提高管理效率
实现

【Originator】

java 复制代码
package com.test.memento.theory;

public class Originator {

/**
 * 角色的状态信息
 */
private String state;

public String getState() {
    return state;
}

public void setState(String state) {
    this.state = state;
}

/**
 * 编写一个方法,可以保存一个状态对象 Memento
 * 因此编写一个方法,返回 Memento
 *
 * @return
 */
public Memento saveStateMemento() {
    return new Memento(state);
}

/**
 * 通过备忘录对象获取原有状态信息,恢复状态
 *
 * @param memento
 */
public void getStateFromMemento(Memento memento) {
    state = memento.getState();
}
}

【Memento】

java 复制代码
package com.test.memento.theory;

public class Memento {
/**
 * 用来保存状态信息
 */
private String state;

/**
 * 构造器
 *
 * @param state
 */
public Memento(String state) {
    super();
    this.state = state;
}

/**
 * 获取保存的状态信息
 * @return
 */
public String getState() {
    return state;
}

}

【Caretaker】

java 复制代码
package com.test.memento.theory;

import java.util.ArrayList;
import java.util.List;

/**
* 统一管理备忘录对象
*/
public class Caretaker {

/**
 * 在 List 集合中会有很多的备忘录对象
 * 如果想要保存多个Originator的多个状态,可以使用HashMap<Originator的ID,List<Memento>>
 */
private List<Memento> mementoList = new ArrayList<Memento>();

public void add(Memento memento) {
    mementoList.add(memento);
}

/**
 * 获取 Originator 的 第 index 个 备忘录对象(即所保存的状态)
 *
 * @param index
 * @return
 */
public Memento get(int index) {
    return mementoList.get(index);
}
}

【主类】

java 复制代码
package com.test.memento.theory;

public class Client {

public static void main(String[] args) {

  Originator originator = new Originator();
  // 备忘录对象管理器
  Caretaker caretaker = new Caretaker();

  originator.setState(" 状态#1 攻击力 100 ");

  //保存了当前的状态
  caretaker.add(originator.saveStateMemento());

  originator.setState(" 状态#2 攻击力 80 ");

  caretaker.add(originator.saveStateMemento());

  originator.setState(" 状态#3 攻击力 50 ");
  caretaker.add(originator.saveStateMemento());

  System.out.println("当前的状态是 =" + originator.getState());

  //希望得到状态 1, 将 originator 恢复到状态1
  originator.getStateFromMemento(caretaker.get(0));
  System.out.println("恢复到状态1 , 当前的状态是 =" + originator.getState());

}

}

【运行】

java 复制代码
当前的状态是 = 状态#3 攻击力 50 
恢复到状态1 , 当前的状态是 = 状态#1 攻击力 100 

Process finished with exit code 0
1234

案例二

类图
实现

【Originator:GameRole】

java 复制代码
package com.test.memento.game;

public class GameRole {

private int vit;
private int def;

/**
 * 创建Memento,即根据当前的状态得到Memento
 *
 * @return
 */
public Memento createMemento() {
    return new Memento(vit, def);
}

/**
 * 从备忘录对象,恢复GameRole的状态
 *
 * @param memento
 */
public void recoverGameRoleFromMemento(Memento memento) {
    this.vit = memento.getVit();
    this.def = memento.getDef();
}

/**
 * 显示当前游戏角色的状态
 */
public void display() {
    System.out.println("游戏角色当前的攻击力:" + this.vit + " 防御力: " + this.def);
}

public int getVit() {
    return vit;
}

public void setVit(int vit) {
    this.vit = vit;
}

public int getDef() {
    return def;
}

public void setDef(int def) {
    this.def = def;
}


}

【Memento】

java 复制代码
package com.test.memento.game;

public class Memento {

/**
 * 攻击力
 */
private int vit;
/**
 * 防御力
 */
private int def;

public Memento(int vit, int def) {
    this.vit = vit;
    this.def = def;
}

public int getVit() {
    return vit;
}

public void setVit(int vit) {
    this.vit = vit;
}

public int getDef() {
    return def;
}

public void setDef(int def) {
    this.def = def;
}

}

【Caretaker】

java 复制代码
package com.test.memento.game;

/**
* 守护者对象, 保存游戏角色的状态
*/
public class Caretaker {

/**
 * 因为大战Boss之前只有一个状态,所以这里保存一次状态,不需要使用集合
 */
private Memento memento;
//对GameRole保存多次状态
//private ArrayList<Memento> mementos;
//对多个GameRole保存多个状态
//private HashMap<String, ArrayList<Memento>> rolesMementos;

public Memento getMemento() {
    return memento;
}

public void setMemento(Memento memento) {
    this.memento = memento;
}

}

【主类】

java 复制代码
package com.test.memento.game;

public class Client {

public static void main(String[] args) {
  //创建游戏角色
  GameRole gameRole = new GameRole();
  gameRole.setVit(100);
  gameRole.setDef(100);

  System.out.println("和boss大战前的状态");
  gameRole.display();

  //把当前状态保存caretaker
  Caretaker caretaker = new Caretaker();
  caretaker.setMemento(gameRole.createMemento());

  System.out.println("和boss大战~~~");
  gameRole.setDef(30);
  gameRole.setVit(30);

  gameRole.display();

  System.out.println("大战后,使用备忘录对象恢复到站前");

  gameRole.recoverGameRoleFromMemento(caretaker.getMemento());
  System.out.println("恢复后的状态");
  gameRole.display();
}

}

【运行】

java 复制代码
和boss大战前的状态
游戏角色当前的攻击力:100 防御力: 100
和boss大战~~~
游戏角色当前的攻击力:30 防御力: 30
大战后,使用备忘录对象恢复到站前
恢复后的状态
游戏角色当前的攻击力:100 防御力: 100

Process finished with exit code 0

案例三

案例说明
  • 游戏是自动进行的
  • 游戏的主人公通过掷骰子来决定下一个状态
  • 当骰子点数为1的时候,主人公的金钱会增加
  • 当骰子点数为2的时候,主人公的金钱会减少
  • 当骰子点数为6的时候,主人公会得到水果
  • 主人公没有钱时游戏就会结束
类图
实现

【Originator:Gamer】

java 复制代码
package com.test.memento.Sample.game;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Random;

public class Gamer {
/**
 * 所持金钱
 */
private int money;
/**
 * 获得的水果
 */
private List fruits = new ArrayList();
/**
 * 随机数生成器
 */
private Random random = new Random();
/**
 * 表示水果种类的数组
 */
private static String[] fruitsName = {
        "苹果", "葡萄", "香蕉", "橘子",
};

/**
 * 构造函数
 *
 * @param money
 */
public Gamer(int money) {
    this.money = money;
}

/**
 * 获取当前所持金钱
 *
 * @return
 */
public int getMoney() {
    return money;
}

/**
 * 投掷骰子进行游戏
 */
public void bet() {
    // 掷骰子,随机获取一个点数
    int dice = random.nextInt(6) + 1;

    if (dice == 1) {
        //  骰子结果为1...增加所持金钱
        money += 100;
        System.out.println("所持金钱增加了。");
    } else if (dice == 2) {
        // 骰子结果为2...所持金钱减半
        money /= 2;
        System.out.println("所持金钱减半了。");
    } else if (dice == 6) {
        // 骰子结果为6...获得水果
        String f = getFruit();
        System.out.println("获得了水果(" + f + ")。");
        fruits.add(f);
    } else {
        // 骰子结果为3、4、5则什么都不会发生
        System.out.println("什么都没有发生。");
    }
}

/**
 * 拍摄快照
 *
 * @return
 */
public Memento createMemento() {
    Memento m = new Memento(money);
    Iterator it = fruits.iterator();
    while (it.hasNext()) {
        String f = (String) it.next();
        if (f.startsWith("好吃的")) {
            // 只保存好吃的水果
            m.addFruit(f);
        }
    }
    return m;
}

/**
 * 撤销到指定状态
 *
 * @param memento
 */
public void restoreMemento(Memento memento) {
    this.money = memento.money;
    this.fruits = memento.getFruits();
}

/**
 * 用字符串输出主人公的状态
 *
 * @return
 */
public String toString() {
    return "[money = " + money + ", fruits = " + fruits + "]";
}

/**
 * 随机获取一个水果
 *
 * @return
 */
private String getFruit() {
    String prefix = "";
    if (random.nextBoolean()) {
        prefix = "好吃的";
    }
    return prefix + fruitsName[random.nextInt(fruitsName.length)];
}
}

【Memento】

java 复制代码
package com.test.memento.Sample.game;

import java.util.ArrayList;
import java.util.List;

public class Memento {
/**
 * 游戏主人公所持金钱
 */
int money;
/**
 * 游戏主人公当前获得的水果
 */
ArrayList fruits;
/**
 * 获取当前所持金钱(narrow interface)
 * narrow interface:Memento角色为外部的 Caretaker 角色提供了"窄接口(API)"。可以通过窄接口(API)获取的Memento角色的内部信息非常有限,因此可以有效地防止信息泄露
 * @return
 */
public int getMoney() {
    return money;
}

/**
 * 构造函数(wide interface),只有相同包的Gamer类才使用该构造方法,因为方法修饰类型是default
 *
 * @param money
 */
Memento(int money) {
    this.money = money;
    this.fruits = new ArrayList();
}

/**
 * 添加水果(wide interface)
 *
 * @param fruit
 */
void addFruit(String fruit) {
    fruits.add(fruit);
}

/**
 * 获取当前所持所有水果(wide interface)
 * wide interface:"宽接口(API)"是指所有用于获取恢复对象状态信息的方法的集合。由于宽接口(API)会暴露所有Memento角色的内部信息,因此能够使用宽接口(API)的只有Originator 角色
 * @return
 */
List getFruits() {
    return (List) fruits.clone();
}
}

【Caretaker】

java 复制代码
package com.test.memento.Sample;


import com.test.memento.Sample.game.Gamer;
import com.test.memento.Sample.game.Memento;

public class Main {
public static void main(String[] args) {
    // 最初的所持金钱数为100
    Gamer gamer = new Gamer(100);
    // 保存最初的状态
    Memento memento = gamer.createMemento();
    for (int i = 0; i < 10; i++) {
        // 显示掷骰子的次数
        System.out.println("==== " + i);
        // 显示主人公现在的状态
        System.out.println("当前状态:" + gamer);

        // 进行游戏
        gamer.bet();

        System.out.println("所持金钱为" + gamer.getMoney() + "元。");

        // 决定如何处理Memento
        if (gamer.getMoney() > memento.getMoney()) {
            System.out.println("    (所持金钱增加了许多,因此保存游戏当前的状态)");
            memento = gamer.createMemento();
        } else if (gamer.getMoney() < memento.getMoney() / 2) {
            System.out.println("    (所持金钱减少了许多,因此将游戏恢复至以前的状态)");
            gamer.restoreMemento(memento);
        }

        // 等待一段时间
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        System.out.println("");
    }
}
}

【运行】

java 复制代码
==== 0
当前状态:[money = 100, fruits = []]
什么都没有发生。
所持金钱为100元。

==== 1
当前状态:[money = 100, fruits = []]
所持金钱增加了。
所持金钱为200元。
(所持金钱增加了许多,因此保存游戏当前的状态)

==== 2
当前状态:[money = 200, fruits = []]
什么都没有发生。
所持金钱为200元。

==== 3
当前状态:[money = 200, fruits = []]
什么都没有发生。
所持金钱为200元。

==== 4
当前状态:[money = 200, fruits = []]
什么都没有发生。
所持金钱为200元。

==== 5
当前状态:[money = 200, fruits = []]
什么都没有发生。
所持金钱为200元。

==== 6
当前状态:[money = 200, fruits = []]
什么都没有发生。
所持金钱为200元。

==== 7
当前状态:[money = 200, fruits = []]
什么都没有发生。
所持金钱为200元。

==== 8
当前状态:[money = 200, fruits = []]
所持金钱减半了。
所持金钱为100元。

==== 9
当前状态:[money = 100, fruits = []]
所持金钱增加了。
所持金钱为200元。


Process finished with exit code 0
拓展

使用序列化(Serialization)功能来将Memento类的实例保存为文件。请修改示例程序以实现下列功能。

  • 在应用程序启动时,如果发现不存在game.dat文件时,以所持金钱数目为100开始游戏;如果发现 game.dat 已经存在,则以文件中所保存的状态开始游戏
  • 当所持金钱大量增加后,将Memento类的实例保存到game.dat文件中
  1. 将Memento继承Serializable接口来实现序列化操作,除此之外,不需要进行其他修改
java 复制代码
public class Memento implements Serializable {}
1
  1. 想要扩展实例保存到文件的功能,只需要修改Caretaker
java 复制代码
package com.test.memento.A4;


import com.test.memento.A4.game.Gamer;
import com.test.memento.A4.game.Memento;

import java.io.*;

public class Main {
/**
 * 数据保存的文件名
 */
public static final String SAVE_FILE_NAME = "game.dat";

public static void main(String[] args) {
    // 最初的所持金钱数为100
    Gamer gamer = new Gamer(100);
    // 从文件中读取起始状态
    Memento memento = loadMemento();
    if (memento != null) {
        System.out.println("读取上次保存存档开始游戏。");
        gamer.restoreMemento(memento);
    } else {
        System.out.println("新游戏。");
        memento = gamer.createMemento();
    }
    for (int i = 0; i < 100; i++) {
        // 显示次数
        System.out.println("==== " + i);
        // 显示当前主人公的状态
        System.out.println("当前状态:" + gamer);
        // 进行游戏
        gamer.bet();   

        System.out.println("所持金钱为" + gamer.getMoney() + "元。");

        // 决定如何处理Memento
        if (gamer.getMoney() > memento.getMoney()) {
            System.out.println("    (所持金钱增加了许多,因此保存游戏当前的状态)");
            memento = gamer.createMemento();
            // 将实例保存至文件中
            saveMemento(memento);
        } else if (gamer.getMoney() < memento.getMoney() / 2) {
            System.out.println("    (所持金钱减少了许多,因此将游戏恢复至以前的状态)");
            gamer.restoreMemento(memento);
        }

        // 等待一段时间
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        System.out.println("");
    }
}

/**
 * 保存实例到文件中
 * @param memento
 */
public static void saveMemento(Memento memento) {   
    try {
        ObjectOutput out = new ObjectOutputStream(new FileOutputStream(SAVE_FILE_NAME));
        out.writeObject(memento);
        out.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

/**
 * 从文件中读取实例
 * @return
 */
public static Memento loadMemento() {               
    Memento memento = null;
    try {
        ObjectInput in = new ObjectInputStream(new FileInputStream(SAVE_FILE_NAME));
        memento = (Memento)in.readObject();
        in.close();
    } catch (FileNotFoundException e) {
        System.out.println(e.toString());
    } catch (IOException e) {
        e.printStackTrace();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    return memento;
}
}

总结

【优点】

  • 给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回退到某个历史的状态
  • 实现了信息的封装,使用户不需要关心状态的保存细节

【缺点】

  • 如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存,这个需要注意。为了节约内存,备忘录模式可以和原型模式配合使用

【适用应用场景】

  • 打游戏时的存档
  • Windows 里的ctrl+z
  • IE 中的后退
  • 数据库的事务管理
相关推荐
萧瑟余晖28 分钟前
JDK 26 新特性详解
java·开发语言
马优晨1 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
维天说4 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json
Zane19944 小时前
并发 vs 并行:别再傻傻分不清了,一文讲透 Java 并发编程的第一课
java·后端
杨充5 小时前
11.DDD与战术建模
设计模式·开源·代码规范
杨充5 小时前
12.综合实战图片框架
设计模式·开源·代码规范
噢,我明白了5 小时前
java中Excel的导入和导出(EasyExcel)
java·开发语言·excel
吠品5 小时前
Zabbix Web界面误报Server未运行的排查与解决
java·服务器·数据库
啊湘5 小时前
天气查询API接口 按月Token鉴权 实时天气 物联网可用 文档齐全
java·后端·struts
Java内核笔记6 小时前
告别十亿美元的错误 : Spring Boot 4 空安全 (JSpecify) 实战
java·spring boot·后端