Skip to content

34备忘录模式:如何在聊天会话中记录历史消息?

相较于其他的设计模式,备忘录模式不算太常用,但好在这个模式理解、掌握起来并不难,代码实现也比较简单,应用场景就更是比较明确和有限,一般应用于编辑器或会话上下文中防丢失、撤销、恢复等场景中。所以,相较前面的课程来说,今天的内容学起来会比较轻松些。

话不多说,让我们开始今天的学习吧。

模式原理分析

备忘录模式的原始定义是:捕获并外部化对象的内部状态,以便以后可以恢复,所有这些都不会违反封装。

这个定义是非常简单的,我们可以直接来看看备忘录模式对应的 UML 图:

备忘录模式的 UML 图

从该 UML 图中,我们能看出备忘录模式包含两个关键角色。

  • 原始对象(Originator):除了创建自身所需要的属性和业务逻辑外,还通过提供方法 create() 和 restore(memento) 来保存和恢复对象副本。

  • 备忘录(Memento):用于保存原始对象的所有属性状态,以便在未来进行撤销操作。

下面我们再来看看 UML 对应的代码实现。首先,我们创建原始对象 Originator,对象中有四个属性,分别是 state 用于显示当前对象状态,id、name、phone 用来模拟业务属性,并添加 get、set 方法、create() 方法用于创建备份对象,restore(memento) 用于恢复对象状态。

java
public class Originator {
    private String state = "原始对象";  //打印当前状态
    private String id;
    private String name;
    private String phone;
    public Originator() {
    }
    //省略get、set方法

    public Memento create() {
        return new Memento(id, name, phone);
    }
    public void restore(Memento m) {
        this.state = m.getState();
        this.id = m.getId();
        this.name = m.getName();
        this.phone = m.getPhone();
    }
    @Override
    public String toString() {
        return "Originator{" +
                "state='" + state + '\'' +
                ", id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", phone='" + phone + '\'' +
                '}';
    }
}

然后,再来创建备忘录对象 Memento,你会发现备忘录对象几乎就和原始对象的属性一模一样。

java
public class Memento {
    private String state = "从备份对象恢复为原始对象";  //打印当前状态
    private String id;
    private String name;
    private String phone;
    public Memento(String id, String name, String phone) {
        this.id = id;
        this.name = name;
        this.phone = phone;
    }
    //省略get、set方法

    @Override
    public String toString() {
        return "Memento{" +
                "state='" + state + '\'' +
                ", id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", phone='" + phone + '\'' +
                '}';
    }
}

接着我们运行一个单元测试,如下代码:

java
public class Demo {
    public static void main(String[] args) {
        Originator originator = new Originator();
        originator.setId("1");
        originator.setName("mickjoust");
        originator.setPhone("1234567890");
        System.out.println(originator);
        Memento memento = originator.create();
        originator.setName("修改");
        System.out.println(originator);
        originator.restore(memento);
        System.out.println(originator);
    }
}
//输出结果
Originator{state='原始对象', id='1', name='mickjoust', phone='1234567890'}
Originator{state='原始对象', id='1', name='修改', phone='1234567890'}
Originator{state='从备份对象恢复为原始对象', id='1', name='mickjoust', phone='1234567890'}

从上面的代码实现和最后输出的结果可以看出,备忘录模式的代码实现其实是非常简单的,关键点就在于要能保证原始对象在某一个时刻的对象状态被完整记录下来

使用场景分析

一般来讲,备忘录模式常用的使用场景有这样几种。

  • 需要保存一个对象在某一个时刻的状态时。比如,在线编辑器中编写文字时断网,需要恢复为断网前的状态。

  • 不希望外界直接访问对象的内部状态时。比如,在包裹配送的过程中,如果从仓库运送到配送站,只需要显示"在运行中",而具体使用汽车还是飞机,这个用户并不需要知道。

为了帮助你更好地理解备忘录模式的适用场景,接下来我们还是通过一个简单的例子来为你演示一下。

假设你现在正在管理一个博客系统,你经常需要创建 Blog 对象,但是有一些作者在写 Blog 的过程中可能会出现断网的情况,这时就需要你保存 Blog 对象在这个时刻的状态信息,后续等作者重新联网后,能够及时地恢复其断网前的状态。那具体该怎么来做呢?

你可以先创建一个 Blog 对象,该对象中包含 id、title 和 content,分别代表了 Blog 的唯一编号、标题和内容;并提供创建备忘录的 createMemento() 和 restore(BlogMemento m) 方法,分别用于创建备忘录和通过备忘录来恢复原始的 Blog 对象。

java
public class Blog {
    private long id;
    private String title;
    private String content;
    public Blog(long id, String title) {
        super();
        this.id = id;
        this.title = title;
    }
    public long getId() {
        return id;
    }
    public void setId(long id) {
        this.id = id;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }
    public BlogMemento createMemento() {
        BlogMemento m = new BlogMemento(id, title, content);
        return m;
    }
    public void restore(BlogMemento m) {
        this.id = m.getId();
        this.title = m.getTitle();
        this.content = m.getContent();
    }
    @Override
    public String toString() {
        return "Blog{" +
                "id=" + id +
                ", title='" + title + '\'' +
                ", content='" + content + '\'' +
                '}';
    }
}

然后,再来创建一个 Blog 的备忘录对象 BlogMemento,同样是复制 Blog 所需要的所有属性内容。

java
public class BlogMemento {
    private final long id;
    private final String title;
    private final String content;
    public BlogMemento(long id, String title, String content) {
        super();
        this.id = id;
        this.title = title;
        this.content = content;
    }
    public long getId() {
        return id;
    }
    public String getTitle() {
        return title;
    }
    public String getContent() {
        return content;
    }
}

这样基于 Blog 对象的备忘录就创建好了。最后,我们依然还是运行一段单元测试代码来看看运行结果。

java
public class Client {
    public static void main(String[] args) {
        Blog blog = new Blog(1, "My Blog");
        blog.setContent("ABC");      //原始的文章内容
        System.out.println(blog);
        BlogMemento memento = blog.createMemento();   //创建blog的备忘录
        blog.setContent("123");      //改变内容
        System.out.println(blog);
        blog.restore(memento);       //撤销操作
        System.out.println(blog);    //这时会显示原始的内容
    }
}

从这个运行结果中,你会发现使用备忘录模式能非常方便地进行撤销操作。当你在编辑文章内容时,其实就是在修改 content 内容,这时备忘录会记录特定时间点里的对象状态,如果这时需要撤销修改,那么就会恢复到原来的对象状态。

所以说,备忘录模式在频繁需要撤销与恢复的场景中能够发挥很好的作用

为什么使用备忘录模式?

分析完备忘录模式的原理和使用场景后,我们再来说说使用备忘录模式的原因,可总结为以下两个。

第一个,为了记录多个时间点的备份数据。 与传统备份不同的是,备忘录模式更多是用来记录多个时间点的对象状态数据,比如编辑器、聊天会话中会涉及多次操作和多次交互对话,一方面是为了记录某个时间点数据以便以后运营用来做数据分析,另一方面是为了能够通过多次数据快照,防止客户端篡改数据。

第二个,需要快速撤销当前操作并恢复到某对象状态。 比如,微信中的撤回功能其实就是备忘录模式的一种体现,用户发错信息后,需要立即恢复到未发送状态。

收益什么?损失什么?

通过上述分析,我们也可以得出备忘录模式主要有以下优点。

  • 能够快速撤消对对象状态的更改。 比如,在编辑器中不小心覆盖了一段重要的文字,使用 undo 操作能够快速恢复这段文字。

  • 能够帮助缓存记录历史对象状态。比如,在客服会话聊天中,对于某一些重要的对话(退换货、价保等),我们会记录这些对象数据,传统的做法是调用一次服务接口,一旦服务出现故障就很容易导致聊天回复速度变慢;而使用备忘录模式则能够记录这些重要的数据信息(用户提供的订单数据),而不需要反复查询接口,这样能提升回复客户的效率。

  • 能够提升代码的扩展性。 备忘录模式是通过增加外部对象来保存原始对象的状态,而不是在原始对象中新增状态记录,当不再需要保存对象状态时就能很方便地取消这个对象。同理,新增备忘录对象也非常容易。

同样,备忘录模式也有一些缺点。

  • 备忘录会破坏封装性。 因为当备忘录在进行恢复的过程中遇见错误时,可能会恢复错误的状态。比如,备份的对象状态中有需要调用数据库等外部服务时,在恢复过程中如果遇见数据库宕机,那么可能恢复的对象数据就会存在错误。

  • 备忘录的对象数据很大时,读取数据可能出现内存用尽的情况。 比如,在编辑器中加入高清的图片,如果直接记录图片本身可能会导致内存被用尽,进而导致系统出现崩溃的情况。

总结

备忘录模式也叫快照模式,具体来说,就是通过捕获对象在某一个时刻的对象状态,再将其保存到外部对象,以便在需要的时候恢复对象到指定时刻下的状态。

备忘录模式的应用场景比较局限,主要是用来备份、撤销、恢复等,这与平时我们常说的"备份"看上去很相似,但实际上两者的差异是很大的:备忘录模式更侧重于代码的设计和实现,支持简单场景中的应用,比如记录 Web 请求中的 header 信息等;而备份更多是以解决方案的形式出现,比如异地容灾备份、数据库主从备份等所支持的是更复杂的业务场景,备份的数据量往往也更大。

因此,你在使用备忘录模式时,一定不要误认为它就是万能的备份模式,要合理评估对象所使用的内存空间,再确定是否使用备忘录模式。

课后思考

在某些时候,我们可能还是需要使用备忘录模式备份大对象,耗时也可能会很长,你有没有好办法来缩短时间消耗呢?欢迎你在留言区与我分享。

在下一讲,我会接着与你分享"中介者模式与解决耦合过多问题"的相关内容,记得按时来听课!