备忘录模式

备忘录模式

标签 : Java与设计模式


备忘录模式: 在不破坏封装性的前提下, 捕获一个对象的内部状态( or 拷贝), 并在该对象之外保存这个状态, 这样以后就可 将该对象恢复到原先保存的状态.
备忘录模式
(图片来源: 设计模式: 可复用面向对象软件的基础)
将保存细节封装在Memento中, 后面即使修改了保存细节也不会影响客户端.


模式实现

案例: 游戏进度保存
在攻击Boss前, 将当前游戏进度保存, 万一失败还可从保存点重新开始.

备忘录模式


Originator-原发器

  • 负责创建一个备忘录Memento, 用以记录当前时刻它的内部状态(决定Memento存储哪些内部状态 -不一定是全部属性);
  • 可使用备忘录恢复内部状态.
/**
 * 游戏角色, 原发器
 *
 * @author jifang
 * @since 16/8/29 上午10:05.
 */
public class GameRoleOriginator {

    private int vit;    // 生命值
    private int atk;    // 攻击力
    private int def;    // 防御力

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

    public void fight() {
        vit -= 10;
        atk -= 8;
        def += 10;
    }

    public RoleStateMemento save() {
        return new RoleStateMemento(vit, atk, def);
    }

    public void recover(RoleStateMemento memento) {
        this.setVit(memento.getVit());
        this.setAtk(memento.getAtk());
        this.setDef(memento.getDef());
    }

    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;
    }

    @Override
    public String toString() {
        return "GameRoleOriginator{" +
                "vit=" + vit +
                ", atk=" + atk +
                ", def=" + def +
                '}';
    }
}

Memento-备忘录

  • 负责存储Originator的内部状态(与Originator共同决定存储原发器哪些内部状态);
  • 防止Originator以外对象访问备忘录. Memento实际应该有两个接口: Caretaker只能看到一个窄接口 -只能将备忘录传递给其他对象. Originator能够看到一个宽接口, 允许它访问返回先前状态所需的所有数据(C++中可由friend提供支持). 理想的情况是只允许生成本备忘录的那个原发器访问本备忘录的内部状态.
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;
    }
}

Caretaker-负责人

  • 负责保存好备忘录Memento;
  • 不能对备忘录内容进行操作或检查.
public class RoleStateCaretaker {

    private Deque<RoleStateMemento> stack = new LinkedList<>();

    public void save(RoleStateMemento memento) {
        stack.push(memento);
    }

    public RoleStateMemento checkout() {
        return stack.pop();
    }
}
  • Client
public class Client {

    @Test
    public void client() {
        RoleStateCaretaker caretaker = new RoleStateCaretaker();

        GameRoleOriginator originator = new GameRoleOriginator(100, 50, 50);
        System.out.println("角色初始状态: " + originator);

        // 保存进度
        caretaker.save(originator.save());

        System.out.println("fight boss...");
        originator.fight();
        System.out.println("阻击Boss后的状态: " + originator);

        originator.recover(caretaker.checkout());
        System.out.println("恢复后的状态: " + originator);
    }
}

序列化所有属性

如果Memento需要保存的是Originator的所有属性, 那么可将Originator的所有属性都存储到一个Map<String, Object>结构中由Caretaker保存, 这样就节省了Memento中间类的开发成本. 甚至还可将Originator序列化为二进制流/字符串存储到持久化设备中(如磁盘、DB、Redis), 节省内存开销, 下面演示将Originator转化为Map<String, Object>存储:

  • Originator
    注意save()/recover()的变化:
public class GameRoleOriginator {

    private int vit;    // 生命值
    private int atk;    // 攻击力
    private int def;    // 防御力

    // ...

    public void fight() {
        vit -= 10;
        atk -= 8;
        def += 10;
    }

    public Map<String, Object> save() {
        try {
            return BeanUtil.bean2Map(this);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }


    public void recover(Map<String, Object> memento) {
        GameRoleOriginator bean;
        try {
            bean = BeanUtil.map2Bean(memento);
        } catch (IllegalAccessException | InstantiationException | NoSuchFieldException e) {
            throw new RuntimeException(e);
        }
        this.setVit(bean.getVit());
        this.setAtk(bean.getAtk());
        this.setDef(bean.getDef());
    }

    // ...
}
  • BeanUtil
public class BeanUtil {

    public static Map<String, Object> bean2Map(Object object) throws IllegalAccessException {
        Map<String, Object> map = new HashMap<>();
        Class<?> clazz = object.getClass();
        map.put("class", clazz);

        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            String key = field.getName();
            Object value = field.get(object);
            map.put(key, value);
        }

        return map;
    }

    public static <T> T map2Bean(Map<String, Object> map) throws IllegalAccessException, InstantiationException, NoSuchFieldException {
        Class<?> clazz = (Class<?>) map.get("class");
        Field[] fields = clazz.getDeclaredFields();
        Object object = clazz.newInstance();
        for (Field field : fields) {
            field.setAccessible(true);
            Object value = map.get(field.getName());
            field.set(object, value);
        }

        return (T) object;
    }
}

小结

  • 适用性

    • 必须保存一个对象在某一个时刻的(部分)状态, 这样以后需要时它才能恢复到先前的状态;
    • 如果一个用接口来让其他对象直接得到这些状态, 将会暴露对象的实现细节并破坏对象的封装性, 而Memento可以把复杂的对象内部信息对其他的对象屏蔽起来, 从而可以恰当的保持封装的边界.
      • 事务回滚;
      • 棋类游戏中的悔棋;
      • PhotoShop的历史记录.
  • 相关模式

    • 命令模式: 如果在使用命令模式时需要实现命令的撤销, 那么可用Memento来存储可撤销的状态.
    • 迭代器模式: 备忘录可用于迭代.

参考:
设计模式: 可复用面向对象软件的基础
大话设计模式
高淇讲设计模式
《JAVA与模式》之备忘录模式
23种设计模式(15):备忘录模式


上一篇:读《DOOM启示录》随想


下一篇:House Robber的解法