设计模式-备忘录模式

文章目录

  • 一、概述
    • [1.1 结构与角色](#1.1 结构与角色)
    • [1.2 适用场景](#1.2 适用场景)
  • 二、实现方式
    • [2.1 白箱实现](#2.1 白箱实现)
    • [2.2 黑箱实现](#2.2 黑箱实现)
    • [2.3 白箱 vs 黑箱](#2.3 白箱 vs 黑箱)
  • 三、总结

一、概述

在软件开发中,经常会遇到这样的场景:需要记录一个对象的内部状态 ,以便在后续某个时刻能够将其恢复到之前的状态。例如,文本编辑器的撤销功能、游戏中的存档/读档、数据库事务的回滚、虚拟机的快照等等。如果不使用设计模式,通常的做法是由外部对象直接访问目标对象的内部字段来保存和恢复状态,但这会严重破坏封装性

java 复制代码
// 反例:外部直接访问内部状态------破坏封装
public class Editor {
    public String content;     // ⚠️ 为了保存状态,不得不暴露为 public
    public int cursorPosition; // ⚠️ 封装被破坏,外部可以随意修改
}

// 外部保存状态
Editor editor = new Editor();
String savedContent = editor.content;        // 直接读取内部字段
int savedPosition = editor.cursorPosition;

// 外部恢复状态
editor.content = savedContent;               // 直接写入内部字段
editor.cursorPosition = savedPosition;

备忘录模式(Memento Pattern)正是为了解决这个问题而诞生的------它在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后可以将该对象恢复到原先保存的状态。备忘录模式将状态的保存和恢复逻辑封装在发起人内部,外部对象只能"保管"备忘录而不能"窥探"其内容。

生活中的备忘录模式例子:

  • 游戏存档:玩家(Caretaker)点击"保存",游戏角色(Originator)将自己的生命值、位置、装备等状态打包成一个存档文件(Memento),玩家无法直接修改存档内容,只能在需要时点击"读档"恢复状态
  • 文本编辑器撤销:编辑器(Originator)每次编辑操作前创建一个快照(Memento),撤销管理器(Caretaker)负责保管快照列表,撤销时编辑器从快照恢复状态
  • 数据库事务回滚:事务开始前保存数据快照(Memento),如果事务失败,根据快照进行回滚
  • 虚拟机快照:虚拟机(Originator)创建快照(Memento),快照管理器(Caretaker)保管快照,可以在任意时刻恢复到某个快照状态

核心:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后可将该对象恢复到原先保存的状态

1.1 结构与角色

备忘录模式包含以下角色:
保管
创建
恢复
请求保存
请求恢复
Caretaker 负责人
Memento 备忘录
Originator 发起人
Caretaker 不能修改 Memento 的内容

Originator 是唯一能访问 Memento 内部状态的类

  • Originator(发起人):需要保存和恢复状态的对象。它创建备忘录对象,并使用备忘录恢复自己的状态。发起人是唯一能访问备忘录内部状态的类
  • Memento(备忘录) :存储发起人的内部状态。备忘录提供两种接口------宽接口 (供发起人访问所有状态)和窄接口(供负责人等外部对象访问,只能看到备忘录的"空壳")
  • Caretaker(负责人):负责保管备忘录,但不能对备忘录的内容进行操作或检查。负责人只负责存储和传递备忘录对象

关键设计 :备忘录模式的核心在于"封装边界"------发起人拥有对备忘录的完全访问权限(宽接口),而负责人只能持有备忘录的引用却不能查看其内容(窄接口)。这确保了发起人的内部状态不会被外部对象随意访问。

1.2 适用场景

  • 需要保存一个对象在某一时刻的状态,以便后续可以恢复到该状态
  • 如果用一个接口来让其他对象直接得到这些状态,将会暴露对象的实现细节并破坏封装性
  • 需要实现撤销(Undo)和恢复(Redo)功能
  • 需要实现事务回滚机制

二、实现方式

备忘录模式的实现有白箱黑箱两种方式。两者的核心区别在于:备忘录存储的状态是否对外部可见------白箱实现中备忘录的状态对负责人也可见,实现简单但封装性较弱;黑箱实现中只有发起人能访问备忘录的状态,封装性更强但实现稍复杂。

以"文本编辑器撤销功能"为例,编辑器需要支持写入文本后可以撤销回之前的状态:

2.1 白箱实现

白箱实现中,备忘录类将状态字段公开,所有对象都可以访问备忘录存储的状态。这种方式实现简单,但封装性较弱------负责人可以"看到"备忘录的内容。
保管 List
createMemento()
restore(memento)
save(memento)
undo() → memento
Caretaker 撤销管理器
Memento 备忘录
Editor 编辑器 Originator
白箱:Memento 的状态字段为 public

Caretaker 可以访问备忘录内容

(1)备忘录------白箱实现

java 复制代码
/**
 * 备忘录:存储编辑器的状态快照
 * 白箱实现:状态字段对外部公开,Caretaker 也能访问
 */
public class EditorMemento {

    /** 文档内容 */
    public final String content;

    /** 光标位置 */
    public final int cursorPosition;

    public EditorMemento(String content, int cursorPosition) {
        this.content = content;
        this.cursorPosition = cursorPosition;
    }
}

(2)发起人------编辑器

java 复制代码
/**
 * 发起人:文本编辑器
 * 负责创建备忘录和从备忘录恢复状态
 */
public class Editor {

    /** 文档内容 */
    private final StringBuilder content = new StringBuilder();

    /** 光标位置 */
    private int cursorPosition = 0;

    /**
     * 在光标位置插入文本
     *
     * @param text 要插入的文本
     */
    public void insert(String text) {
        if (text == null || text.isEmpty()) {
            return;
        }
        content.insert(cursorPosition, text);
        cursorPosition += text.length();
        System.out.println("插入「" + text + "」→ 当前内容:" + getContent() + ",光标:" + cursorPosition);
    }

    /**
     * 删除光标位置前的指定长度文本
     *
     * @param length 删除长度
     */
    public void delete(int length) {
        if (length <= 0 || cursorPosition == 0) {
            return;
        }
        int start = Math.max(0, cursorPosition - length);
        String deleted = content.substring(start, cursorPosition);
        content.delete(start, cursorPosition);
        cursorPosition = start;
        System.out.println("删除「" + deleted + "」→ 当前内容:" + getContent() + ",光标:" + cursorPosition);
    }

    /**
     * 创建备忘录------保存当前状态
     *
     * @return 当前状态的快照
     */
    public EditorMemento createMemento() {
        System.out.println("创建快照:content=\"" + getContent() + "\", cursor=" + cursorPosition);
        return new EditorMemento(getContent(), cursorPosition);
    }

    /**
     * 从备忘录恢复状态
     *
     * @param memento 备忘录快照
     */
    public void restore(EditorMemento memento) {
        if (memento == null) {
            return;
        }
        content.setLength(0);
        content.append(memento.content);
        cursorPosition = memento.cursorPosition;
        System.out.println("恢复快照:content=\"" + getContent() + "\", cursor=" + cursorPosition);
    }

    public String getContent() {
        return content.toString();
    }

    public int getCursorPosition() {
        return cursorPosition;
    }
}

(3)负责人------撤销管理器

java 复制代码
import java.util.Stack;

/**
 * 负责人:撤销管理器
 * 负责保管备忘录对象,管理撤销和恢复操作
 * 白箱实现下可以访问备忘录内容,但通常不应修改
 */
public class UndoManager {

    /** 撤销栈:记录已保存的快照 */
    private final Stack<EditorMemento> undoStack = new Stack<>();

    /** 恢复栈:记录已撤销的快照 */
    private final Stack<EditorMemento> redoStack = new Stack<>();

    /**
     * 保存当前状态
     *
     * @param memento 当前状态的备忘录
     */
    public void save(EditorMemento memento) {
        undoStack.push(memento);
        redoStack.clear(); // 保存新快照后清空恢复栈
    }

    /**
     * 撤销------恢复到上一个快照
     *
     * @param editor 编辑器
     */
    public void undo(Editor editor) {
        if (undoStack.isEmpty()) {
            System.out.println("没有可撤销的操作");
            return;
        }
        // 1. 将当前状态保存到恢复栈
        redoStack.push(editor.createMemento());
        // 2. 从撤销栈弹出上一个快照并恢复
        EditorMemento memento = undoStack.pop();
        editor.restore(memento);
    }

    /**
     * 恢复------重做已撤销的操作
     *
     * @param editor 编辑器
     */
    public void redo(Editor editor) {
        if (redoStack.isEmpty()) {
            System.out.println("没有可恢复的操作");
            return;
        }
        // 1. 将当前状态保存到撤销栈
        undoStack.push(editor.createMemento());
        // 2. 从恢复栈弹出快照并恢复
        EditorMemento memento = redoStack.pop();
        editor.restore(memento);
    }
}

(4)客户端调用

java 复制代码
public class WhiteBoxMementoDemo {
    public static void main(String[] args) {
        Editor editor = new Editor();
        UndoManager undoManager = new UndoManager();

        // 初始状态保存
        undoManager.save(editor.createMemento());

        // 编辑操作
        editor.insert("Hello");
        undoManager.save(editor.createMemento());

        editor.insert(" World");
        undoManager.save(editor.createMemento());

        editor.insert("!");
        undoManager.save(editor.createMemento());

        System.out.println("\n--- 开始撤销 ---");
        undoManager.undo(editor);  // 撤销 "!"
        undoManager.undo(editor);  // 撤销 " World"
        // 当前内容:Hello,光标:5

        System.out.println("\n--- 开始恢复 ---");
        undoManager.redo(editor);  // 恢复 " World"
        // 当前内容:Hello World,光标:11

        // 白箱的缺点:负责人可以直接访问备忘录的内部状态
        EditorMemento peek = undoManager.undoStack.peek();
        System.out.println("\n【白箱问题】负责人可以直接读取备忘录内容:" + peek.content);
    }
}

白箱特点:实现简单直观,备忘录的状态字段对外公开。但缺点是封装性弱------负责人(UndoManager)可以随意访问和修改备忘录的内部状态,违反了备忘录模式"不破坏封装性"的初衷。

2.2 黑箱实现

黑箱实现通过内部类来实现严格的封装------备忘录对象对外部完全是不透明的,只有发起人能访问其内部状态。负责人只能持有备忘录的引用,无法窥探其内容。
保管 List
createMemento()
实现
restore(memento)
save(memento)
undo() → memento
Caretaker 撤销管理器
IMemento 窄接口
Editor 编辑器 Originator
EditorMemento 内部类
黑箱:IMemento 只暴露空接口

只有 Originator 能向下转型访问内部状态

(1)备忘录窄接口------外部只能看到空壳

java 复制代码
/**
 * 备忘录窄接口
 * 对外部(Caretaker)只暴露空接口,不暴露任何状态
 * 只有发起人(Originator)知道具体实现类并可以访问内部状态
 */
public interface IMemento {
    // 空接口------外部对象只能持有引用,不能访问任何内容
}

(2)发起人------黑箱编辑器

java 复制代码
/**
 * 发起人:文本编辑器(黑箱实现)
 * 使用内部类实现备忘录,外部完全无法访问备忘录的内部状态
 */
public class Editor {

    /** 文档内容 */
    private final StringBuilder content = new StringBuilder();

    /** 光标位置 */
    private int cursorPosition = 0;

    /**
     * 在光标位置插入文本
     *
     * @param text 要插入的文本
     */
    public void insert(String text) {
        if (text == null || text.isEmpty()) {
            return;
        }
        content.insert(cursorPosition, text);
        cursorPosition += text.length();
        System.out.println("插入「" + text + "」→ 当前内容:" + getContent() + ",光标:" + cursorPosition);
    }

    /**
     * 删除光标位置前的指定长度文本
     *
     * @param length 删除长度
     */
    public void delete(int length) {
        if (length <= 0 || cursorPosition == 0) {
            return;
        }
        int start = Math.max(0, cursorPosition - length);
        String deleted = content.substring(start, cursorPosition);
        content.delete(start, cursorPosition);
        cursorPosition = start;
        System.out.println("删除「" + deleted + "」→ 当前内容:" + getContent() + ",光标:" + cursorPosition);
    }

    /**
     * 创建备忘录------保存当前状态
     *
     * @return 备忘录(对外部暴露为 IMemento 窄接口)
     */
    public IMemento createMemento() {
        System.out.println("创建快照:content=\"" + getContent() + "\", cursor=" + cursorPosition);
        return new EditorMemento(getContent(), cursorPosition);
    }

    /**
     * 从备忘录恢复状态
     * 只有发起人能向下转型为具体实现类来访问内部状态
     *
     * @param memento 备忘录(IMemento 窄接口)
     */
    public void restore(IMemento memento) {
        if (!(memento instanceof EditorMemento)) {
            throw new IllegalArgumentException("非法的备忘录类型");
        }
        EditorMemento em = (EditorMemento) memento;
        content.setLength(0);
        content.append(em.content);
        cursorPosition = em.cursorPosition;
        System.out.println("恢复快照:content=\"" + getContent() + "\", cursor=" + cursorPosition);
    }

    public String getContent() {
        return content.toString();
    }

    public int getCursorPosition() {
        return cursorPosition;
    }

    /**
     * 备忘录具体实现------内部类
     * 只有 Editor 能访问这个内部类的字段
     */
    private static class EditorMemento implements IMemento {

        /** 文档内容(private,外部不可访问) */
        private final String content;

        /** 光标位置(private,外部不可访问) */
        private final int cursorPosition;

        EditorMemento(String content, int cursorPosition) {
            this.content = content;
            this.cursorPosition = cursorPosition;
        }
    }
}

(3)负责人------黑箱撤销管理器

java 复制代码
import java.util.Stack;

/**
 * 负责人:撤销管理器(黑箱实现)
 * 只持有 IMemento 窄接口的引用,完全无法访问备忘录的内部状态
 */
public class UndoManager {

    /** 撤销栈 */
    private final Stack<IMemento> undoStack = new Stack<>();

    /** 恢复栈 */
    private final Stack<IMemento> redoStack = new Stack<>();

    /**
     * 保存当前状态
     *
     * @param memento 备忘录(IMemento 窄接口)
     */
    public void save(IMemento memento) {
        undoStack.push(memento);
        redoStack.clear();
    }

    /**
     * 撤销
     *
     * @param editor 编辑器
     */
    public void undo(Editor editor) {
        if (undoStack.isEmpty()) {
            System.out.println("没有可撤销的操作");
            return;
        }
        // 1. 保存当前状态
        redoStack.push(editor.createMemento());
        // 2. 恢复上一个快照
        IMemento memento = undoStack.pop();
        editor.restore(memento);
    }

    /**
     * 恢复
     *
     * @param editor 编辑器
     */
    public void redo(Editor editor) {
        if (redoStack.isEmpty()) {
            System.out.println("没有可恢复的操作");
            return;
        }
        // 1. 保存当前状态
        undoStack.push(editor.createMemento());
        // 2. 恢复已撤销的快照
        IMemento memento = redoStack.pop();
        editor.restore(memento);
    }
}

(4)客户端调用

java 复制代码
public class BlackBoxMementoDemo {
    public static void main(String[] args) {
        Editor editor = new Editor();
        UndoManager undoManager = new UndoManager();

        // 初始状态保存
        undoManager.save(editor.createMemento());

        // 编辑操作
        editor.insert("Hello");
        undoManager.save(editor.createMemento());

        editor.insert(" World");
        undoManager.save(editor.createMemento());

        editor.insert("!");
        undoManager.save(editor.createMemento());

        System.out.println("\n--- 开始撤销 ---");
        undoManager.undo(editor);  // 撤销 "!"
        undoManager.undo(editor);  // 撤销 " World"
        // 当前内容:Hello,光标:5

        System.out.println("\n--- 开始恢复 ---");
        undoManager.redo(editor);  // 恢复 " World"
        // 当前内容:Hello World,光标:11

        // 黑箱的优势:负责人无法访问备忘录的内部状态
        // IMemento memento = undoManager.undoStack.peek();
        // memento.content;  // 编译错误!IMemento 是空接口,没有 content 字段
        System.out.println("\n【黑箱优势】负责人完全无法访问备忘录的内部状态,封装性得到保障");
    }
}

黑箱特点 :通过 IMemento 窄接口 + 内部类实现严格的封装。负责人只能持有 IMemento 引用,完全无法访问内部状态。发起人通过 instanceof 检查和向下转型获取具体实现类来访问状态。虽然代码稍复杂,但真正实现了"不破坏封装性"的核心目标。

2.3 白箱 vs 黑箱

对比维度 白箱实现 黑箱实现
封装性 弱,备忘录状态对外部公开 强,外部完全无法访问内部状态
实现复杂度 简单,字段 public 即可 较复杂,需要窄接口 + 内部类
类型安全 编译期无保障 编译期保障(IMemento 空接口)
多发起人支持 需为每种发起人创建独立的备忘录类 每个发起人通过自己的内部类实现
内存开销 相同 相同(内部类持有外部类引用时稍多)

选型建议:

  • 如果对封装性要求不高------选择白箱实现,代码简洁直观
  • 如果严格遵循设计原则、要求不破坏封装性------选择黑箱实现
  • 实际开发中,黑箱实现更为推荐,因为备忘录模式的核心价值就是"不破坏封装性"

三、总结

备忘录模式的核心思想是在不破坏封装性的前提下,捕获一个对象的内部状态并在该对象之外保存,以便之后可以恢复到之前的状态。

优点:

  • 不破坏封装性:备忘录对象对外部不透明,只有发起人能访问其内部状态,保护了发起人的封装边界
  • 简化发起人:发起人不需要管理和维护历史状态,只需提供创建快照和恢复快照的方法
  • 状态恢复安全:通过备忘录对象恢复状态,避免了外部直接操作发起人内部字段的风险
  • 支持多次撤销/恢复:配合 Caretaker 可以轻松实现多步撤销与恢复

缺点:

  • 内存开销大:每次保存状态都会创建一个备忘录对象,如果状态数据量大或频繁保存,会消耗大量内存
  • Caretaker 管理成本:负责人需要维护备忘录的生命周期,及时清理不需要的备忘录以避免内存泄漏
  • 可能暴露实现细节:白箱实现中备忘录的状态对外公开,封装性较弱
  • 语言支持要求:黑箱实现依赖内部类等语言特性,在某些语言中可能难以做到完美的封装

适用场景:

  • 需要保存一个对象在某一时刻的状态,以便后续恢复到该状态
  • 不希望用接口来让其他对象直接得到对象的状态(会破坏封装)
  • 需要实现撤销(Undo)和恢复(Redo)功能
  • 需要实现事务回滚或系统快照功能

白箱 vs 黑箱:白箱实现简单但封装性弱(备忘录字段公开),黑箱通过窄接口 + 内部类实现严格封装------外部只能看到空接口,只有发起人能向下转型访问内部状态。实际开发中推荐黑箱实现,因为"不破坏封装性"正是备忘录模式的设计初衷。


参考博客:

备忘录模式 | 菜鸟教程:https://www.runoob.com/design-pattern/memento-pattern.html

相关推荐
雪度娃娃4 小时前
行为型设计模式——中介者模式
microsoft·设计模式·中介者模式
多加点辣也没关系4 小时前
设计模式-中介者模式
设计模式·中介者模式
geovindu1 天前
go: Read-Write Lock Pattern
开发语言·后端·设计模式·golang·读写锁模式
行走的陀螺仪1 天前
[特殊字符] JavaScript 设计模式完全指南:从入门到精通(含20种模式)
开发语言·javascript·设计模式
小陶来咯1 天前
AI Agent 设计模式:ReAct 深度解析
人工智能·react.js·设计模式
多加点辣也没关系1 天前
设计模式-责任链模式
设计模式·责任链模式
多加点辣也没关系1 天前
设计模式-命令模式
设计模式·命令模式
benpaodeDD1 天前
视频49——设计模式之责任链模式
设计模式·责任链模式
雪度娃娃1 天前
行为型设计模式——迭代器模式
c++·设计模式·迭代器模式