设计模式(20):备忘录模式

「尺有所短,寸有所长;不忘初心,方得始终。」

一、备忘录模式是什么

在日常生活中,我们经常使用备忘录来记录一些比较重要的或者容易遗忘的信息,在计算机应用中,很多软件都有撤销当前操作的功能,这都是备忘录模式的一种实现。

  • 【定义】

    在不破坏封装性的前提下,允许在不暴露对象实现细节的情况下「保存和恢复」对象之前的状态。

  • 「特性」

    备忘录模式又叫做快照模式(Snapshot Pattern)或Token模式,属于行为型模式。

  • 「主要作用」

    • 在对象之外保存对象的某个状态。
    • 用户取消操作时回复到保存的状态。设计模式(20):备忘录模式

二、备忘录模式的适用场景

  • 【适用场景】

    • 需要保存和恢复数据的相关状态场景。
    • 提供一个可回滚(rollback)的操作。
  • 【生活实例】

    • 数据库连接的事务管理。
    • Word、记事本等回撤功能。
    • 浏览器的回退
    • 游戏的存档功能
    • 棋类游戏中的悔棋功能

三、备忘录模式结构

备忘录模式的核心是设计「备忘录类」以及用于「管理备忘录的管理者类」

  • 「发起人(Originator)角色」:记录当前时刻的内部状态,提供创建备忘录和恢复备忘录数据的功能,允许访问返回到先前状态所需的所有数据。实现其他业务功能,它可以访问备忘录里的所有信息。
  • 「备忘录(Memento)角色」:负责存储发起人的内部状态,在需要的时候「提供发起人需要的内部状态」
  • 「管理者(Caretaker)角色」:持有一个备忘录对象的引用。对备忘录进行管理、保存和提供备忘录,其不能对备忘录的内容进行访问与修改,只能将备忘录传递给其他角色。
  • 「客户端(Client)角色」:负责创建发起人和管理者角色对象。
设计模式(20):备忘录模式

四、备忘录模式实现方式

  • 创建备忘录类,声明对应每个状态成员变量的备忘录成员变量。
  • 创建管理者类,持有一个备忘录的引用,当备忘的对象时多个时,持有的是一个数组对象,定义添加,获取某一个备忘录对象的方法,
  • 创建发起者类,定义创建备忘录,记录当前时刻自身的内部状态,通过备忘录恢复内部状态
  • 客户端负责创建发起人和管理者角色对象。

五、备忘录模式的实现

【案例】:下象棋游戏中的悔棋功能

【案例说明】:下象棋的游戏中都有悔棋的功能,这个功能中记录每走一步棋所在位置的状态是用备忘录角色,移动棋子的时发起人角色,管理者负责往备忘录角色中添加棋子的轨迹

  • 「发起人(Originator)角色」

    /**
    * @describe 发起人(Originator)角色
    * @author Edwin
    * @date 2021/11/18 19:46
    */

    public class Originator {

    /**
    * 当前棋子名字
    */

    public String pieceName;
    /**
    * 当前棋子位置
    */

    public String location;

    /**
    * @describe 设置当前棋子的状态
    * @author Edwin
    * @date 2021/11/18 19:52
    */

    public void setOriginatorStatus(String pieceName,String location){
    this.pieceName = pieceName;
    this.location = location;
    }

    /**
    * @describe 工厂方法,返回一个备忘录对象
    * @author Edwin
    * @date 2021/11/18 19:49
    */

    public Memento createMemento(String pieceName,String location){
    return new Memento(pieceName,location);
    }

    /**
    * @describe 将棋子的状态回滚到上一步
    * @author Edwin
    * @date 2021/11/18 19:56
    */

    public void rollback(Memento memento){
    setOriginatorStatus(memento.getPieceName(),memento.getLocation());
    }

    }
  • 「备忘录(Memento)角色」

    /**
    * 备忘录(Memento)角色 :棋子轨迹
    */

    @Data
    public class Memento {
    /**
    * 棋子名字
    */

    public String pieceName;
    /**
    * 棋子位置
    */

    public String location;

    public Memento(String pieceName, String location) {
    this.pieceName = pieceName;
    this.location = location;
    }
    }
  • 「管理者(Caretaker)角色」

    /**
    * @describe 管理者(Caretaker)角色
    * @author Edwin
    * @date 2021/11/18 19:43
    */

    public class Caretaker {

    /**
    * 持有一组备忘录对象的引用 这里有多个棋子轨迹的备忘录,
    * 使用Java栈(先进后出)实现最后一步走得棋子,最先后悔
    * @author Edwin
    * @date 2021/11/18 19:41
    */

    private Stack stack;

    public Caretaker() {
    this.stack = new Stack();
    }


    /**
    * 往栈里面放一个Memento Memento 记录着每一步的棋子的轨迹
    * @author Edwin
    * @date 2021/11/18 19:41
    */

    public void push(Memento memento) {
    stack.push(memento);
    }

    /**
    * 从栈里面取出一个Memento 悔棋
    * @author Edwin
    * @date 2021/11/18 19:40
    */

    public Memento pop() {
    if (stack.empty()) {
    //栈空了!已经是第一步了,没办法悔棋了!
    return null;
    }
    return (Memento)stack.pop();
    }
    }
  • 「客户端代码实现」

    public class MementoMain {

    public static void main(String[] args) {
    //负责创建发起人和管理者角色对象。
    Originator originator = new Originator();
    Caretaker caretaker = new Caretaker();
    // 设置棋子初始位置
    originator.setOriginatorStatus("车","K1");
    System.out.println("棋子初始位置:棋子:" + originator.pieceName + "的位置是:" + originator.location);
    // 备份棋子初始位置信息
    caretaker.push(originator.createMemento(originator.pieceName,originator.location));
    // 走第一步棋
    originator.setOriginatorStatus("车","K10");
    System.out.println("第一步:棋子:" + originator.pieceName + "的位置是:" + originator.location);
    // 备份第一步的棋子信息
    caretaker.push(originator.createMemento(originator.pieceName,originator.location));
    // 走第二步棋
    originator.setOriginatorStatus("车","K20");
    System.out.println("第二步:棋子:" + originator.pieceName + "的位置是:" + originator.location);
    // 备份第二步的棋子信息
    caretaker.push(originator.createMemento(originator.pieceName,originator.location));
    // 走第三步棋
    originator.setOriginatorStatus("车","K30");
    System.out.println("第三步:棋子:" + originator.pieceName + "的位置是:" + originator.location);
    //开始悔棋
    for (int i = 1; i < 1000; i++) {
    //模拟一直悔棋,知道不能反悔为止
    Memento pop = caretaker.pop();
    if(pop != null){
    originator.setOriginatorStatus(pop.getPieceName(),pop.getLocation());
    System.out.println("第" + i + "次悔棋后:棋子:" + originator.pieceName + "的位置是:" + originator.location);
    }else {
    System.out.println("第" + i + "次悔棋后:现在已经是第一步,没有棋可以悔了,当前棋子:" + originator.pieceName + "的位置是:" + originator.location);
    break;
    }
    }
    }
    }
  • 「案例输出结果」

设计模式(20):备忘录模式

六、备忘录模式的优缺点

  • 「优点」

    • 提供了一种可以恢复状态的机制。可以将数据恢复到用户需要的某个历史的状态。
    • 备忘录模式复杂的发起人内部信息对其他的对象屏蔽起来,实现了内部状态的封装。
    • 符合「单一职责原则」,发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理。
  • 「缺点」

    当保存的内部状态信息过多或者特别频繁时,内存资源消耗大。

七、备忘录模式和其他模式的联系

  • 可以同时使用【命令模式】和【备忘录模式】来实现「撤销」。命令用于对目标对象执行各种不同的操作, 备忘录用来保存一条命令执行前该对象的状态。

  • 可以同时使用【备忘录模式】和【迭代器模式】来获取当前迭代器的状态, 并且在需要的时候进行回滚。

  • 【原型模式】可以作为【备忘录】的一个简化版本,在备忘录模式中,通过定义「备忘录」来备份「发起人」的信息,  而原型模式的 clone() 方法具有自备份功能,只需要发起人实现 Cloneable 接口就有备份自己的功能,此时可以删除备忘录类。

设计模式(20):备忘录模式

八、总结

8.1 备忘录模式两个概念

  • 「窄接口」

    只允许它把备忘录对象传给其他的对象。针对的是负责人对象和其他除发起人对象之外的任何对象。

  • 「宽接口」

    允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态。针对发起人。

8.2 备忘录模式四种模式

  • 白箱模式

    将发起人角色的状态存储在一个所有对象都看得到的地方,备忘录内部所存储的状态对所有对象公开。

  • 黑箱模式

    备忘录角色对发起人角色对象提供一个宽接口,而为其他对象提供一个窄接口。简单说就是将备忘录定义为一个接口,实现类放到发起人里面,外界不可见。

  • 多重检查点

    常见的系统往往需要存储不止一个状态,而是需要存储多个状态,或者叫做有多个检查点。用list或者map存储,这种叫做多重检查点

  • 自述历史模式

    发起人角色自己兼任管理者角色。将管理者的行为在发起人中实现,通过发起人自行维护历史状态。


原文始发于微信公众号(星河之码):设计模式(20):备忘录模式

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/27093.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!