设计模式-备忘录模式(Memento)

1. 概念

  • 备忘录模式(Memento Pattern)是一种行为型设计模式,它允许在不破坏对象封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样,以后就可以将该对象恢复到原先保存的状态。该模式属于行为型模式,主要目的是提供一种可以恢复对象状态的机制。

2. 原理结构图


设计模式-备忘录模式(Memento)_第1张图片

  1. 发起人(Originator)
    • 角色定义:记录当前时刻的内部状态,并可以恢复到一个先前状态(即它之前保存至备忘录的状态)。
    • 主要责任
      • 创建一个备忘录对象,用于记录当前时刻的内部状态。
      • 使用备忘录对象来恢复内部状态。
    • 关注点:关注内部状态的保存与恢复,但不需要关心备忘录对象的具体存储和管理。
  2. 管理者(Caretaker)
    • 角色定义:负责保存备忘录对象,并且在需要的时候提供备忘录对象给发起人。
    • 主要责任
      • 在发起人请求时提供备忘录对象给发起人保存其当前状态。
      • 在发起人需要时,提供之前保存的备忘录对象以恢复状态。
    • 关注点:关注备忘录对象的存储和提供,但不关心备忘录对象的具体内容。
  3. 备忘录(Memento)
    • 角色定义:负责存储发起人对象的内部状态,并且可以防止发起人以外的其他对象访问备忘录的内容。
    • 主要责任
      • 存储发起人对象的内部状态。
      • 提供一种方式来恢复发起人对象的内部状态。
    • 关注点:关注状态的存储和恢复,同时保护这些状态不被未授权的对象访问。

3. 代码示例

3.1 示例1-文本编辑器中的撤销功能
  • 在文本编辑器中实现撤销功能是一个典型的备忘录模式应用场景。以下是一个简化的Java代码示例,用于演示如何使用备忘录模式来实现文本编辑器的撤销功能。
import java.util.ArrayList;
import java.util.List;

// 备忘录接口
interface Memento {
    void setState(String state);
    String getState();
}

// 具体的备忘录类,保存文本编辑器的状态
class TextEditorMemento implements Memento {
    private String content;

    public TextEditorMemento(String content) {
        this.content = content;
    }

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

    @Override
    public String getState() {
        return this.content;
    }
}

// 发起人类,即文本编辑器
class TextEditor {
    private String content;
    private List<Memento> history;

    public TextEditor() {
        this.content = "";
        this.history = new ArrayList<>();
    }

    // 写入文本
    public void write(String text) {
        this.content += text;
        // 创建备忘录并保存当前状态
        Memento memento = new TextEditorMemento(this.content);
        this.history.add(memento);
    }

    // 获取当前文本
    public String getContent() {
        return this.content;
    }

    // 撤销操作
    public void undo() {
        if (!this.history.isEmpty()) {
            // 弹出最后一个备忘录
            this.history.remove(this.history.size() - 1);
            // 从备忘录恢复状态
            this.content = this.history.size() > 0 ? this.history.get(this.history.size() - 1).getState() : "";
        }
    }
}
public class MemoPatternDemo {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();

        editor.write("Hello, ");
        editor.write("world!");
        // 输出:Hello, world!
        System.out.println("Current content: " + editor.getContent()); 

        editor.undo();
        // 输出:Hello,
        System.out.println("After undo: " + editor.getContent()); 

        editor.undo();
        // 输出:空字符串
        System.out.println("After undo 2: " + editor.getContent()); 
    }
}
  • 将看到如下输出:
Current content: Hello, world!
After undo: Hello, 
After undo 2: 
  • 请注意,这个示例为了简洁起见省略了一些重要细节,比如异常处理、更完整的编辑器功能以及可能的并发问题。在实际应用中,你还需要考虑这些因素来构建一个健壮和完整的文本编辑器撤销功能。

3.2 示例2
  • 浏览器中的后退功能是一个非常适合应用备忘录模式的场景。在这个场景中,每当用户访问一个网页时,浏览器的历史记录就会保存一个当前页面的状态(通常是URL)。当用户点击后退按钮时,浏览器会从历史记录中取出前一个页面的状态,并恢复到那个状态。
  • 以下是一个简化的Java代码实现,展示了如何使用备忘录模式来支持浏览器中的后退功能:
import java.io.Serializable;
import java.util.Deque;
import java.util.LinkedList;

interface Memento {
    String getUrl();
    // 可以添加其他状态信息,比如页面标题、滚动位置等
}


class BrowserHistoryMemento implements Memento, Serializable {
    private String url;

    public BrowserHistoryMemento(String url) {
        this.url = url;
    }

    @Override
    public String getUrl() {
        return url;
    }

    // 省略其他可能的状态信息
}

interface Originator {
    Memento createMemento();

    void setMemento(Memento memento);
    // 其他浏览器功能的方法,如加载页面等
}


class WebBrowser implements Originator {
    private String currentUrl;
    private Deque<Memento> historyStack = new LinkedList<>();

    public WebBrowser(String initialUrl) {
        this.currentUrl = initialUrl;
    }

    @Override
    public Memento createMemento() {
        return new BrowserHistoryMemento(currentUrl);
    }

    @Override
    public void setMemento(Memento memento) {
        if (memento != null && memento instanceof BrowserHistoryMemento) {
            BrowserHistoryMemento browserHistoryMemento = (BrowserHistoryMemento) memento;
            this.currentUrl = browserHistoryMemento.getUrl();
        }
    }

    public void loadUrl(String url) {
        this.currentUrl = url;
        // 在访问新页面之前,先将当前页面的状态压入历史栈
        historyStack.push(createMemento());
    }

    public void goBack() {
        if (!historyStack.isEmpty()) {
            // 从历史栈中弹出最后一个状态,并获取上一个值
            historyStack.pop();
            setMemento(historyStack.getLast());
        }
    }

    // 获取当前URL的方法,用于显示或其他操作
    public String getCurrentUrl() {
        return currentUrl;
    }
}

public class BrowserDemo {
    public static void main(String[] args) {
        WebBrowser browser = new WebBrowser("https://www.example.com");

        // 模拟用户访问几个页面
        browser.loadUrl("https://www.google.com");
        browser.loadUrl("https://www.facebook.com");

        // 模拟用户点击后退按钮
        browser.goBack();

        // 显示当前页面的URL,应该是用户之前访问的页面
        System.out.println("Current URL after going back: " + browser.getCurrentUrl());
        // 应该输出 "https://www.google.com"
    }
}
  • 将看到如下输出:
Current URL after going back: https://www.google.com
  • 在这个示例中,使用了Deque(双端队列)来作为历史栈,因为它支持在队列两端进行添加和移除操作,非常适合实现浏览器的历史记录功能。每当用户加载一个新的页面时,我们就将当前页面的状态压入历史栈;当用户点击后退按钮时,就从历史栈中弹出最后一个状态,并恢复到那个状态。
  • 请注意,这个示例为了简洁起见省略了一些重要细节,比如异常处理、页面内容的加载和渲染、多标签页的支持以及可能的并发问题。在实际应用中,还需要考虑这些因素来构建一个功能完善的浏览器后退功能。

4. 优缺点

  • 主要作用
    • 备忘录模式的主要作用是在不破坏对象封装性的前提下,保存对象的某个状态,以便在未来某个时刻恢复该对象到此状态。
  • 优点
    • 状态恢复能力:备忘录模式允许对象在不破坏封装性的前提下保存其内部状态,并在需要时恢复到这些状态。这种能力特别适用于那些需要频繁回退或撤销操作的应用场景,比如文本编辑器、游戏等。通过保存和恢复状态,用户可以轻松地撤销之前的操作,提高了用户体验。
    • 封装性保护:备忘录模式通过引入备忘录对象来保存状态信息,而不需要暴露发起人的内部细节。这使得发起人对象可以保持其封装性,不会被外部对象直接访问和修改其内部状态。这有助于保护对象的状态安全,防止意外修改或破坏。
    • 降低耦合度:在备忘录模式中,发起人对象和管理者对象之间的耦合度较低。发起人对象只需要提供创建备忘录和恢复状态的方法,而不需要关心备忘录的具体存储和管理。管理者对象负责保存和提供备忘录对象,但不需要了解发起人对象的内部实现。这种松耦合的设计使得代码更加灵活和可维护。
    • 支持多次恢复:通过保存多个不同时刻的备忘录对象,备忘录模式支持对象恢复到不同的历史状态。这使得应用可以更加灵活地处理用户操作,提供更加丰富的功能和体验。
  • 缺点
    • 资源消耗:当发起人角色的状态需要完整地存储到备忘录对象中时,如果状态信息复杂或庞大,将会占用大量的存储空间。这可能导致在资源有限的环境中,如移动设备或嵌入式系统,出现性能问题或存储限制。
    • 管理复杂性:备忘录模式引入了一个新的角色——管理者,用于保存和提供备忘录对象。这增加了系统的复杂性,特别是在大型系统中,需要额外管理备忘录对象的存储、检索和过期等问题。
    • 状态同步问题:如果发起人对象的状态在多个地方被修改,而备忘录对象没有及时更新,那么恢复状态可能会导致数据不一致或冲突。因此,需要确保备忘录对象的更新与发起人对象的状态变化保持同步,这增加了开发的复杂性和潜在的错误风险。
    • 撤销操作的局限性:备忘录模式主要关注状态的保存和恢复,但对于复杂的撤销操作(如跨多个对象的撤销或条件性撤销)可能不够灵活。在某些情况下,可能需要结合其他设计模式或机制来实现更高级的撤销功能。

5. 应用场景

5.1 主要包括以下几个方面
  1. 撤销操作:在需要支持撤销功能的系统中,如文本编辑、绘图工具等,使用备忘录模式可以方便地保存历史状态,实现撤销功能。
  2. 状态恢复:对于需要记录并恢复对象历史状态的场景,如游戏存档、业务流程管理等,备忘录模式能够确保状态的完整性和可恢复性。
  3. 资源管理:在某些需要管理资源状态的场景中,可以使用备忘录模式来保存和恢复资源状态。例如,在操作系统中,资源管理器可能需要保存当前资源分配状态,以便在需要时能够恢复到之前的状态,确保系统的稳定性和可靠性。
  4. 事务处理:在数据库或分布式系统中,利用备忘录模式可以在事务失败时回滚到之前的状态,保证数据的一致性和完整性。

5.2 实际应用
  1. 文本编辑器中的撤销功能:在编写文档时,用户经常需要撤销之前的输入或删除操作。通过备忘录模式,编辑器可以保存用户的每一步操作,当用户选择撤销时,可以恢复到之前的状态。
  2. 游戏中的存档和读档功能:玩家在进行游戏时,可能会希望随时保存当前的游戏进度,以便在之后能够继续游戏。通过备忘录模式,游戏可以保存玩家的状态和游戏进度,并在玩家需要时恢复这些状态。
  3. 浏览器中的后退功能:当用户在浏览网页时,可能会通过点击链接跳转到其他页面。浏览器使用备忘录模式来保存用户的浏览历史,当用户点击后退按钮时,可以恢复到之前浏览的页面。
  4. 数据库事务处理:在进行数据库操作时,一系列操作可能需要作为一个整体来执行。如果其中某个操作失败,整个事务都应该回滚到操作之前的状态。通过使用备忘录模式,可以保存事务开始时的状态,并在需要时回滚到该状态。
  5. 软件中的“撤销”操作:许多软件都提供了撤销功能,允许用户撤销之前的操作。这些软件使用备忘录模式来记录用户的每一步操作,以便在用户选择撤销时能够恢复到之前的状态。

6. JDK中的使用

  • java.util.Stack
    • 发起人(Originator):在这个案例中,Stack类本身充当发起人角色。它维护了一个元素列表来表示栈的状态。当调用push方法添加元素时,Stack会更新其内部状态。
    • 备忘录(Memento):Stack类中的每个元素都可以看作是一个备忘录,因为它们记录了栈在某个特定时刻的状态。当执行push操作时,新元素被添加到栈顶,相当于创建了一个新的状态快照。
    • 看护者(Caretaker):虽然Stack类本身管理着元素的存储和访问,但在这个案例中,看护者角色可以由使用Stack的客户端代码承担。客户端代码决定何时对Stack执行push或pop操作,从而保存或恢复特定的状态。

在这个案例中,Stack类提供了search方法,允许客户端代码查找并恢复之前的栈状态。这是通过内部维护一个索引来实现的,该索引指向当前“记住”的栈顶元素。当调用search方法时,Stack会将索引移动到指定位置,然后可以通过pop操作恢复到该状态。


7. 注意事项

  • 状态封装:确保发起人对象的状态被妥善封装,避免外部直接访问或修改。
  • 接口一致性:备忘录对象与发起人对象的状态接口应保持一致,以便恢复时无缝对接。
  • 资源消耗:注意备忘录对象可能占用大量资源,需合理管理存储和销毁。
  • 安全性:防止备忘录对象被不当修改或滥用,确保状态恢复的正确性。
  • 避免泄露:备忘录内容可能包含敏感信息,需防止信息泄露风险。
  • 撤销操作的限制:备忘录模式主要用于保存和恢复状态,对于复杂的撤销逻辑可能不适用。

遵循这些注意事项,可以更有效地应用备忘录模式,实现状态管理的灵活性和可靠性。


你可能感兴趣的:(设计模式,设计模式,备忘录模式)