备忘录模式的最初描述是:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可以将该对象恢复到原先保存的状态。

核心思想:提供一种状态撤销/恢复机制,同时保持对象内部的封装性,不让外部直接访问内部状态。

通俗的讲,就是提供提供撤销 / 回滚 / 存档恢复功能,但绝对不泄露对象的内部隐私数据,外部不能乱改内部属性。

备忘录模式包括三种角色:原发者(需要在某个时刻保存其状态的对象。原发者负责创建备忘录,然后原发者使用该备忘录记录自己的状态),备忘录(负责存储原发者状态的对象),负责人(负责管理保存备忘录对象)。

逻辑解析

public class Originator {
    // 核心:需要保存/恢复的内部状态(私有)
    private String state;

    // 修改自身状态
    public void setState(String state) {
        this.state = state;
    }

    public String getState() {
        return state;
    }

    // 关键方法1:创建备忘录 → 把自己当前状态打包,生成快照
    public Memento createMemento() {
        return new Memento(state);
    }

    // 关键方法2:恢复备忘录 → 拿着快照,把自己状态改回去
    public void restoreMemento(Memento memento) {
        this.state = memento.getState();
    }
}
public class Memento {
    // final 不可变:一旦存档,状态不能被篡改,安全
    private final String state;

    // 构造方法:只能由发起人传入状态
    public Memento(String state) {
        this.state = state;
    }

    // 只有包级私有getter,没有setter
    // 外部类无法修改、随意获取内部状态
    String getState() {
        return state;
    }
}
public class Caretaker {
    // 集合:存放多条备忘录 = 多份历史存档
    private final List<Memento> mementoList = new ArrayList<>();

    // 新增一份存档
    public void addMemento(Memento memento) {
        mementoList.add(memento);
    }

    // 根据索引,取出指定历史存档
    public Memento getMemento(int index) {
        return mementoList.get(index);
    }
}

下面我们写个简单的例子

 1.Snapshot.java - 备忘录(Memento)

作用:存储 TextEditor 的内部状态快照

import java.util.Date;

// 快照(Memento)
class Snapshot {
    private final String content;
    private final Date timestamp;

    public Snapshot(String content) {
        this.content = content;
        this.timestamp = new Date();
    }

    public String getContent() {
        return content;
    }

    public Date getTimestamp() {
        return timestamp;
    }
}

第1行:导入 Date 类,用于记录快照创建的时间戳
第4行:定义 Snapshot 类,这是备忘录角色
第5行:private final String content - 用私有不可变字段存储文本内容
final 确保快照一旦创建就不能被修改(保证状态的稳定性)
第6行:private final Date timestamp - 记录快照的创建时间
第8-11行:构造函数
接收一个字符串参数作为内容
自动记录当前时间作为时间戳
第13-15行:getContent() 方法 - 提供只读访问内容
第17-19行:getTimestamp() 方法 - 提供只读访问时间戳
设计要点:

只提供 getter,没有 setter → 防止外部篡改快照
使用 final 修饰符 → 确保不可变性

2.TextEditor.java - 发起人(Originator)

作用:这是需要保存状态的核心对象,负责创建和恢复快照

// 文本编辑器(Originator)
class TextEditor {
    private StringBuilder content = new StringBuilder();

    public void write(String text) {
        content.append(text);
    }

    public void deleteLast(int n) {
        if (n <= content.length()) {
            content.delete(content.length() - n, content.length());
        }
    }

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

    // 保存状态
    public Snapshot save() {
        return new Snapshot(content.toString());
    }

    // 恢复状态
    public void restore(Snapshot snapshot) {
        this.content = new StringBuilder(snapshot.getContent());
    }
}

第2行:定义 TextEditor 类,这是发起人角色
第3行:private StringBuilder content - 使用 StringBuilder 存储文本内容
选择 StringBuilder 而非 String 是因为它支持高效的可变字符串操作
第5-7行:write() 方法 - 向编辑器追加文本
调用 content.append(text) 将新文本添加到末尾
第9-13行:deleteLast() 方法 - 删除末尾的 n 个字符
第10行:检查要删除的字符数是否不超过当前长度(防止越界)
第11行:content.delete(起始位置, 结束位置)
content.length() - n 计算删除的起始位置
content.length() 是删除的结束位置(即末尾)
第15-17行:getContent() 方法 - 返回当前内容的字符串形式
调用 toString() 将 StringBuilder 转换为 String
第20-22行:save() 方法 - 创建快照的关键方法
调用 new Snapshot(content.toString()) 创建当前状态的副本
重要:传递的是字符串副本,不是引用,避免后续修改影响快照
第25-27行:restore() 方法 - 恢复状态的关键方法
从快照中获取保存的内容
用该内容创建新的 StringBuilder 替换当前内容
这样编辑器就回到了快照时的状态
设计要点:
save() 和 restore() 是唯一与 Snapshot 交互的方法
 通过字符串副本隔离状态,防止引用共享问题

3. HistoryManager.java - 负责人(Caretaker)
作用:管理快照的历史记录,但不查看或修改快照内容

import java.util.Stack;

// 历史记录管理器(Caretaker)
class HistoryManager {
    private final Stack<Snapshot> undoStack = new Stack<>();

    public void push(Snapshot snapshot) {
        undoStack.push(snapshot);
    }

    public Snapshot pop() {
        if (!undoStack.isEmpty()) {
            return undoStack.pop();
        }
        return null;
    }
}

第1行:导入 Stack 类(栈数据结构)
第4行:定义 HistoryManager 类,这是负责人角色
第5行:private final Stack<Snapshot> undoStack - 用栈存储快照
使用栈的原因:撤销操作是**后进先出(LIFO)**的
最近保存的状态最先被撤销
final 确保栈对象本身不被替换
第7-9行:push() 方法 - 将快照压入栈顶
每次编辑后调用此方法保存状态
第11-16行:pop() 方法 - 弹出栈顶的快照
第12行:检查栈是否为空(防御性编程)
第13行:如果非空,弹出并返回栈顶快照
第15行:如果为空,返回 null(表示无法撤销)
设计要点:
 HistoryManager 不知道 Snapshot 的内部结构
 只负责存储和提供快照,符合"单一职责原则"

4.Client.java - 客户端使用示例
作用:演示如何使用这三个类实现撤销功能

// 客户端使用
public class Client {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();
        HistoryManager history = new HistoryManager();

        editor.write("Hello");
        history.push(editor.save());

        editor.write(" World");
        history.push(editor.save());

        editor.write("!");
        System.out.println("当前内容: " + editor.getContent()); // Hello World!

        // 撤销两次
        editor.restore(history.pop());
        System.out.println("撤销1: " + editor.getContent()); // Hello World

        editor.restore(history.pop());
        System.out.println("撤销2: " + editor.getContent()); // Hello
    }
}

第4行:创建文本编辑器实例
第5行:创建历史记录管理器实例
第7-8行:第一次编辑和保存
写入 "Hello"
调用 editor.save() 创建快照,并通过 history.push() 保存
第10-11行:第二次编辑和保存
写入 " World"(注意前面的空格)
再次保存快照
第13-14行:第三次编辑但不保存
写入 "!"
打印当前内容:"Hello World!"
第17-18行:第一次撤销
history.pop() 弹出最近的快照(保存的是 "Hello World" 状态)
editor.restore() 恢复到该状态
打印:"Hello World"
第20-21行:第二次撤销
弹出下一个快照(保存的是 "Hello" 状态)
恢复到该状态
打印:"Hello"

操作                    | 编辑器内容      | 栈中的快照
------------------------|----------------|------------------
write("Hello")          | Hello          | [空]
save() & push()         | Hello          | [Hello]
write(" World")         | Hello World    | [Hello]
save() & push()         | Hello World    | [Hello, Hello World]
write("!")              | Hello World!   | [Hello, Hello World]
pop() & restore()       | Hello World    | [Hello]
pop() & restore()       | Hello          | [空]

设计模式的核心思想总结
为什么要这样设计?
封装性保护--TextEditor 的内部实现(StringBuilder)对外部完全隐藏,Snapshot 只暴露必要的 getter,不提供 setter
职责分离--TextEditor:负责业务逻辑(编辑文本),Snapshot:负责状态存储,HistoryManager:负责历史管理
开闭原则--如果要添加"重做"功能,只需扩展 HistoryManager,无需修改现有代码
安全性--快照是不可变的(final),防止意外修改,通过字符串副本隔离状态

更多推荐