备忘录模式(Memento Pattern)详解

文章目录

    • [1. 什么是备忘录模式?](#1. 什么是备忘录模式?)
    • [2. 为什么需要备忘录模式?](#2. 为什么需要备忘录模式?)
    • [3. 备忘录模式的核心概念](#3. 备忘录模式的核心概念)
    • [4. 备忘录模式的结构](#4. 备忘录模式的结构)
    • [5. 备忘录模式的基本实现](#5. 备忘录模式的基本实现)
      • [5.1 简单的文本编辑器示例](#5.1 简单的文本编辑器示例)
    • [6. 备忘录模式的进阶实现](#6. 备忘录模式的进阶实现)
      • [6.1 游戏角色状态保存示例](#6.1 游戏角色状态保存示例)
      • [6.2 备忘录模式的不同实现方式](#6.2 备忘录模式的不同实现方式)
    • [7. 备忘录模式的复杂实现](#7. 备忘录模式的复杂实现)
      • [7.1 多状态备忘录](#7.1 多状态备忘录)
      • [7.2 使用内部类实现](#7.2 使用内部类实现)
    • [8. 备忘录模式在Java中的实际应用](#8. 备忘录模式在Java中的实际应用)
      • [8.1 Java中的序列化与备忘录模式](#8.1 Java中的序列化与备忘录模式)
      • [8.2 Java Swing中的撤销/重做功能](#8.2 Java Swing中的撤销/重做功能)
    • [9. 备忘录模式与其他设计模式的结合](#9. 备忘录模式与其他设计模式的结合)
      • [9.1 备忘录模式与命令模式](#9.1 备忘录模式与命令模式)
    • [10. 备忘录模式的优缺点](#10. 备忘录模式的优缺点)
      • [10.1 优点](#10.1 优点)
      • [10.2 缺点](#10.2 缺点)
    • [11. 备忘录模式的适用场景](#11. 备忘录模式的适用场景)
    • [12. 备忘录模式与其他模式的比较](#12. 备忘录模式与其他模式的比较)
      • [12.1 备忘录模式 vs 原型模式](#12.1 备忘录模式 vs 原型模式)
      • [12.2 备忘录模式 vs 命令模式](#12.2 备忘录模式 vs 命令模式)
      • [12.3 备忘录模式 vs 状态模式](#12.3 备忘录模式 vs 状态模式)
    • [13. 备忘录模式的常见问题与解决方案](#13. 备忘录模式的常见问题与解决方案)
      • [13.1 如何减少备忘录模式的内存消耗?](#13.1 如何减少备忘录模式的内存消耗?)
      • [13.2 如何实现多层撤销/重做?](#13.2 如何实现多层撤销/重做?)
      • [13.3 如何处理复杂对象的状态?](#13.3 如何处理复杂对象的状态?)
    • [14. 总结](#14. 总结)
      • [14.1 核心要点](#14.1 核心要点)
      • [14.2 设计建议](#14.2 设计建议)

1. 什么是备忘录模式?

备忘录模式是一种行为型设计模式,它允许在不破坏对象封装性的前提下,捕获对象的内部状态,并在需要时将对象恢复到这个状态。简单来说,备忘录模式就是用来实现"撤销"和"恢复"功能的一种设计模式。

备忘录模式使得程序可以回到某个历史状态,就像"时间旅行"一样,让我们能够重返过去的某个时间点。

2. 为什么需要备忘录模式?

在以下情况下,备忘录模式特别有用:

  1. 需要保存和恢复对象的状态:当我们需要提供撤销操作或回滚机制时
  2. 直接访问对象的成员变量会破坏封装性:我们需要保存对象状态,但又不希望破坏封装性
  3. 需要保存对象状态的快照:当我们需要暂时保存某些状态以便后期恢复时
  4. 事务回滚:当执行一系列操作后需要回滚到原始状态时
  5. 状态历史记录:当需要维护对象历史状态以便浏览或回溯时

3. 备忘录模式的核心概念

备忘录模式涉及三个核心角色:

  1. 发起人(Originator)

    • 需要保存状态的对象
    • 负责创建一个备忘录,存储自身的内部状态
    • 可以使用备忘录恢复自身状态
  2. 备忘录(Memento)

    • 存储发起人的内部状态
    • 只能被发起人访问,对其他对象不可见
    • 保护内部状态不被外部修改
  3. 管理者(Caretaker)

    • 负责保存备忘录
    • 不能对备忘录的内容进行操作或检查
    • 仅仅是一个存储备忘录的容器

4. 备忘录模式的结构

备忘录模式的UML类图如下:

复制代码
+---------------+       创建       +---------------+
| Originator    |--------------->| Memento        |
+---------------+                +---------------+
| state         |                | state         |
| createMemento()|               +---------------+
| restoreMemento()|                    ^
+---------------+                      |
        ^                              |
        |                              |
        |         保存/获取             |
        |                              |
+---------------+                      |
| Caretaker     |----------------------+
+---------------+
| mementoList   |
+---------------+

5. 备忘录模式的基本实现

5.1 简单的文本编辑器示例

下面是一个简单的文本编辑器示例,展示了备忘录模式的基本实现。

首先,定义备忘录类:

java 复制代码
// 备忘录类 - 存储文本编辑器的状态
public class TextEditorMemento {
    private final String text;
    
    public TextEditorMemento(String text) {
        this.text = text;
    }
    
    // 只允许发起人访问状态
    protected String getText() {
        return text;
    }
}

然后,定义发起人类(文本编辑器):

java 复制代码
// 发起人类 - 文本编辑器
public class TextEditor {
    private String text;
    
    public void setText(String text) {
        this.text = text;
    }
    
    public String getText() {
        return text;
    }
    
    // 创建备忘录,保存当前状态
    public TextEditorMemento save() {
        return new TextEditorMemento(text);
    }
    
    // 从备忘录恢复状态
    public void restore(TextEditorMemento memento) {
        this.text = memento.getText();
    }
}

最后,定义管理者类:

java 复制代码
// 管理者类 - 维护备忘录历史
public class TextEditorCaretaker {
    private final List<TextEditorMemento> history = new ArrayList<>();
    private int currentIndex = -1;
    
    // 保存备忘录
    public void save(TextEditorMemento memento) {
        // 如果当前不是最后一个状态,则删除当前状态之后的所有状态
        if (currentIndex < history.size() - 1) {
            history.subList(currentIndex + 1, history.size()).clear();
        }
        history.add(memento);
        currentIndex = history.size() - 1;
    }
    
    // 撤销操作,返回上一个状态
    public TextEditorMemento undo() {
        if (currentIndex <= 0) {
            return null; // 无法撤销
        }
        currentIndex--;
        return history.get(currentIndex);
    }
    
    // 重做操作,恢复到下一个状态
    public TextEditorMemento redo() {
        if (currentIndex >= history.size() - 1) {
            return null; // 无法重做
        }
        currentIndex++;
        return history.get(currentIndex);
    }
}

创建一个测试类,演示备忘录模式:

java 复制代码
public class TextEditorDemo {
    public static void main(String[] args) {
        // 创建文本编辑器(发起人)
        TextEditor editor = new TextEditor();
        // 创建历史记录管理者(管理者)
        TextEditorCaretaker caretaker = new TextEditorCaretaker();
        
        // 编辑文本并保存状态
        editor.setText("这是第一行文本");
        caretaker.save(editor.save());
        System.out.println("当前文本: " + editor.getText());
        
        // 继续编辑
        editor.setText("这是第一行文本\n这是第二行文本");
        caretaker.save(editor.save());
        System.out.println("当前文本: " + editor.getText());
        
        // 再次编辑
        editor.setText("这是第一行文本\n这是第二行文本\n这是第三行文本");
        caretaker.save(editor.save());
        System.out.println("当前文本: " + editor.getText());
        
        // 执行撤销操作
        TextEditorMemento previousState = caretaker.undo();
        if (previousState != null) {
            editor.restore(previousState);
            System.out.println("\n执行撤销后,文本变为: \n" + editor.getText());
        }
        
        // 再次撤销
        previousState = caretaker.undo();
        if (previousState != null) {
            editor.restore(previousState);
            System.out.println("\n再次撤销后,文本变为: \n" + editor.getText());
        }
        
        // 执行重做
        TextEditorMemento nextState = caretaker.redo();
        if (nextState != null) {
            editor.restore(nextState);
            System.out.println("\n执行重做后,文本变为: \n" + editor.getText());
        }
        
        // 再次重做
        nextState = caretaker.redo();
        if (nextState != null) {
            editor.restore(nextState);
            System.out.println("\n再次重做后,文本变为: \n" + editor.getText());
        }
    }
}

输出结果:

复制代码
当前文本: 这是第一行文本
当前文本: 这是第一行文本
这是第二行文本
当前文本: 这是第一行文本
这是第二行文本
这是第三行文本

执行撤销后,文本变为: 
这是第一行文本
这是第二行文本

再次撤销后,文本变为: 
这是第一行文本

执行重做后,文本变为: 
这是第一行文本
这是第二行文本

再次重做后,文本变为: 
这是第一行文本
这是第二行文本
这是第三行文本

6. 备忘录模式的进阶实现

6.1 游戏角色状态保存示例

下面我们实现一个更复杂的例子:游戏角色状态的保存和恢复。

首先,定义备忘录类:

java 复制代码
// 游戏角色状态备忘录
public class RoleStateMemento {
    private final int vitality;    // 生命力
    private final int attack;      // 攻击力
    private final int defense;     // 防御力
    
    public RoleStateMemento(int vitality, int attack, int defense) {
        this.vitality = vitality;
        this.attack = attack;
        this.defense = defense;
    }
    
    // 提供保护性的访问方法
    protected int getVitality() {
        return vitality;
    }
    
    protected int getAttack() {
        return attack;
    }
    
    protected int getDefense() {
        return defense;
    }
}

然后,定义发起人类(游戏角色):

java 复制代码
// 游戏角色类(发起人)
public class GameRole {
    private String name;           // 角色名称
    private int vitality;          // 生命力
    private int attack;            // 攻击力
    private int defense;           // 防御力
    
    public GameRole(String name) {
        this.name = name;
        this.vitality = 100;       // 初始生命值
        this.attack = 50;          // 初始攻击力
        this.defense = 30;         // 初始防御力
    }
    
    // 创建备忘录,保存当前状态
    public RoleStateMemento saveState() {
        return new RoleStateMemento(vitality, attack, defense);
    }
    
    // 从备忘录恢复状态
    public void restoreState(RoleStateMemento memento) {
        this.vitality = memento.getVitality();
        this.attack = memento.getAttack();
        this.defense = memento.getDefense();
    }
    
    // 战斗,消耗生命值和防御力,提高攻击力
    public void fight() {
        this.vitality -= 30;
        this.attack += 20;
        this.defense -= 10;
    }
    
    // 使用药水,恢复生命值
    public void usePotion() {
        this.vitality += 40;
    }
    
    // 显示角色状态
    public void displayState() {
        System.out.println("角色: " + name);
        System.out.println("生命力: " + vitality);
        System.out.println("攻击力: " + attack);
        System.out.println("防御力: " + defense);
        System.out.println("--------------------");
    }
}

最后,定义管理者类:

java 复制代码
// 游戏存档管理器(管理者)
public class GameStateCaretaker {
    private Map<String, RoleStateMemento> saveSlots = new HashMap<>();
    
    // 保存游戏状态到指定存档
    public void saveState(String slotName, RoleStateMemento memento) {
        saveSlots.put(slotName, memento);
    }
    
    // 从指定存档加载游戏状态
    public RoleStateMemento loadState(String slotName) {
        return saveSlots.get(slotName);
    }
    
    // 显示所有存档
    public void displaySaveSlots() {
        System.out.println("可用存档: ");
        for (String slotName : saveSlots.keySet()) {
            System.out.println("- " + slotName);
        }
        System.out.println("--------------------");
    }
}

创建测试类,演示游戏存档功能:

java 复制代码
public class GameDemo {
    public static void main(String[] args) {
        // 创建游戏角色
        GameRole knight = new GameRole("骑士");
        // 创建游戏存档管理器
        GameStateCaretaker caretaker = new GameStateCaretaker();
        
        // 显示初始状态
        System.out.println("游戏开始,初始状态:");
        knight.displayState();
        
        // 保存初始状态
        caretaker.saveState("初始存档", knight.saveState());
        
        // 进行一场战斗
        System.out.println("进行一场战斗后:");
        knight.fight();
        knight.displayState();
        
        // 保存战斗后状态
        caretaker.saveState("战斗后存档", knight.saveState());
        
        // 使用药水恢复
        System.out.println("使用药水恢复后:");
        knight.usePotion();
        knight.displayState();
        
        // 保存使用药水后状态
        caretaker.saveState("恢复后存档", knight.saveState());
        
        // 显示所有存档
        caretaker.displaySaveSlots();
        
        // 从初始存档加载
        System.out.println("加载初始存档:");
        knight.restoreState(caretaker.loadState("初始存档"));
        knight.displayState();
        
        // 从战斗后存档加载
        System.out.println("加载战斗后存档:");
        knight.restoreState(caretaker.loadState("战斗后存档"));
        knight.displayState();
        
        // 从恢复后存档加载
        System.out.println("加载恢复后存档:");
        knight.restoreState(caretaker.loadState("恢复后存档"));
        knight.displayState();
    }
}

输出结果:

复制代码
游戏开始,初始状态:
角色: 骑士
生命力: 100
攻击力: 50
防御力: 30
--------------------

进行一场战斗后:
角色: 骑士
生命力: 70
攻击力: 70
防御力: 20
--------------------

使用药水恢复后:
角色: 骑士
生命力: 110
攻击力: 70
防御力: 20
--------------------

可用存档: 
- 战斗后存档
- 初始存档
- 恢复后存档
--------------------

加载初始存档:
角色: 骑士
生命力: 100
攻击力: 50
防御力: 30
--------------------

加载战斗后存档:
角色: 骑士
生命力: 70
攻击力: 70
防御力: 20
--------------------

加载恢复后存档:
角色: 骑士
生命力: 110
攻击力: 70
防御力: 20
--------------------

6.2 备忘录模式的不同实现方式

备忘录模式有两种主要实现方式:黑箱模式和白箱模式。

黑箱模式

在黑箱实现中,备忘录类对外隐藏所有状态信息,只有发起人可以访问这些信息。

java 复制代码
// 黑箱模式的备忘录类
public class BlackBoxMemento {
    // 私有的状态字段
    private final String state;
    
    public BlackBoxMemento(String state) {
        this.state = state;
    }
    
    // 外部类只能看到这是一个备忘录对象,无法访问其内容
    // 只有发起人可以通过强制类型转换恢复状态
}

// 发起人类
public class Originator {
    private String state;
    
    public void setState(String state) {
        this.state = state;
    }
    
    // 创建备忘录
    public BlackBoxMemento saveToMemento() {
        return new BlackBoxMemento(state);
    }
    
    // 恢复状态 - 发起人知道备忘录的内部结构
    public void restoreFromMemento(BlackBoxMemento memento) {
        // 此处发起人可以访问备忘录内部状态
        this.state = ((BlackBoxMemento)memento).state;
    }
}
白箱模式

在白箱实现中,备忘录类提供公共的接口来访问其状态,但这样做会破坏封装性。

java 复制代码
// 白箱模式的备忘录类
public class WhiteBoxMemento {
    private String state;
    
    public WhiteBoxMemento(String state) {
        this.state = state;
    }
    
    // 提供公共访问方法
    public String getState() {
        return state;
    }
    
    public void setState(String state) {
        this.state = state;
    }
}

// 发起人类
public class Originator {
    private String state;
    
    public void setState(String state) {
        this.state = state;
    }
    
    // 创建备忘录
    public WhiteBoxMemento saveToMemento() {
        return new WhiteBoxMemento(state);
    }
    
    // 恢复状态
    public void restoreFromMemento(WhiteBoxMemento memento) {
        this.state = memento.getState();
    }
}

一般来说,黑箱模式更符合封装原则,但在Java中实现起来可能会更复杂。而白箱模式虽然违反了封装原则,但实现更简单直接。

7. 备忘录模式的复杂实现

7.1 多状态备忘录

有时我们需要保存对象的多个属性,并且希望能够选择性地恢复这些属性。下面是一个多状态备忘录的实现示例:

java 复制代码
// 多状态备忘录类 - 可以选择性保存和恢复状态
public class DocumentMemento {
    private final String content;        // 文档内容
    private final String title;          // 文档标题
    private final String formatting;     // 文档格式
    private final Date lastModified;     // 最后修改时间
    
    // 构造函数
    public DocumentMemento(String content, String title, String formatting, Date lastModified) {
        this.content = content;
        this.title = title;
        this.formatting = formatting;
        this.lastModified = lastModified != null ? new Date(lastModified.getTime()) : null;
    }
    
    // 获取状态的保护性方法
    protected String getContent() {
        return content;
    }
    
    protected String getTitle() {
        return title;
    }
    
    protected String getFormatting() {
        return formatting;
    }
    
    protected Date getLastModified() {
        return lastModified != null ? new Date(lastModified.getTime()) : null;
    }
}

发起人类(文档):

java 复制代码
// 文档类 - 发起人
public class Document {
    private String content;        // 文档内容
    private String title;          // 文档标题
    private String formatting;     // 文档格式
    private Date lastModified;     // 最后修改时间
    
    public Document(String title) {
        this.title = title;
        this.content = "";
        this.formatting = "normal";
        this.lastModified = new Date();
    }
    
    // 添加内容
    public void addContent(String newContent) {
        this.content += newContent;
        this.lastModified = new Date();
    }
    
    // 改变标题
    public void changeTitle(String newTitle) {
        this.title = newTitle;
        this.lastModified = new Date();
    }
    
    // 改变格式
    public void changeFormatting(String newFormatting) {
        this.formatting = newFormatting;
        this.lastModified = new Date();
    }
    
    // 显示文档信息
    public void display() {
        System.out.println("文档: " + title);
        System.out.println("最后修改时间: " + lastModified);
        System.out.println("格式: " + formatting);
        System.out.println("内容: \n" + content);
        System.out.println("--------------------");
    }
    
    // 创建完整备忘录
    public DocumentMemento createFullMemento() {
        return new DocumentMemento(content, title, formatting, lastModified);
    }
    
    // 创建只包含内容的备忘录
    public DocumentMemento createContentMemento() {
        return new DocumentMemento(content, title, null, null);
    }
    
    // 创建只包含格式的备忘录
    public DocumentMemento createFormattingMemento() {
        return new DocumentMemento(null, null, formatting, null);
    }
    
    // 从备忘录恢复状态 - 选择性恢复
    public void restoreFromMemento(DocumentMemento memento) {
        if (memento.getContent() != null) {
            this.content = memento.getContent();
        }
        
        if (memento.getTitle() != null) {
            this.title = memento.getTitle();
        }
        
        if (memento.getFormatting() != null) {
            this.formatting = memento.getFormatting();
        }
        
        if (memento.getLastModified() != null) {
            this.lastModified = memento.getLastModified();
        } else {
            // 如果没有恢复时间,则更新为当前时间
            this.lastModified = new Date();
        }
    }
}

管理者类:

java 复制代码
// 文档版本管理器 - 管理者
public class DocumentCaretaker {
    // 使用Map存储不同类型的备忘录
    private Map<String, List<DocumentMemento>> versionHistory = new HashMap<>();
    
    // 初始化版本历史记录
    public DocumentCaretaker() {
        versionHistory.put("full", new ArrayList<>());
        versionHistory.put("content", new ArrayList<>());
        versionHistory.put("formatting", new ArrayList<>());
    }
    
    // 保存完整版本
    public void saveFullVersion(DocumentMemento memento) {
        versionHistory.get("full").add(memento);
    }
    
    // 保存内容版本
    public void saveContentVersion(DocumentMemento memento) {
        versionHistory.get("content").add(memento);
    }
    
    // 保存格式版本
    public void saveFormattingVersion(DocumentMemento memento) {
        versionHistory.get("formatting").add(memento);
    }
    
    // 获取最新的完整版本
    public DocumentMemento getLatestFullVersion() {
        List<DocumentMemento> fullVersions = versionHistory.get("full");
        if (fullVersions.isEmpty()) {
            return null;
        }
        return fullVersions.get(fullVersions.size() - 1);
    }
    
    // 获取最新的内容版本
    public DocumentMemento getLatestContentVersion() {
        List<DocumentMemento> contentVersions = versionHistory.get("content");
        if (contentVersions.isEmpty()) {
            return null;
        }
        return contentVersions.get(contentVersions.size() - 1);
    }
    
    // 获取最新的格式版本
    public DocumentMemento getLatestFormattingVersion() {
        List<DocumentMemento> formattingVersions = versionHistory.get("formatting");
        if (formattingVersions.isEmpty()) {
            return null;
        }
        return formattingVersions.get(formattingVersions.size() - 1);
    }
    
    // 显示所有版本历史数量
    public void displayVersionInfo() {
        System.out.println("版本历史信息:");
        System.out.println("完整版本数: " + versionHistory.get("full").size());
        System.out.println("内容版本数: " + versionHistory.get("content").size());
        System.out.println("格式版本数: " + versionHistory.get("formatting").size());
        System.out.println("--------------------");
    }
}

测试类:

java 复制代码
public class MultiStateDocumentDemo {
    public static void main(String[] args) {
        // 创建文档
        Document document = new Document("未命名文档");
        // 创建文档版本管理器
        DocumentCaretaker caretaker = new DocumentCaretaker();
        
        // 显示初始状态
        System.out.println("文档初始状态:");
        document.display();
        
        // 保存初始完整版本
        caretaker.saveFullVersion(document.createFullMemento());
        
        // 添加内容
        document.addContent("这是第一段内容。");
        document.display();
        
        // 保存内容版本
        caretaker.saveContentVersion(document.createContentMemento());
        
        // 改变标题和内容
        document.changeTitle("我的文档");
        document.addContent("\n这是第二段内容。");
        document.display();
        
        // 保存完整版本
        caretaker.saveFullVersion(document.createFullMemento());
        
        // 改变格式
        document.changeFormatting("bold");
        document.display();
        
        // 保存格式版本
        caretaker.saveFormattingVersion(document.createFormattingMemento());
        
        // 显示版本信息
        caretaker.displayVersionInfo();
        
        // 恢复到最新的内容版本(只恢复内容,不恢复标题和格式)
        System.out.println("恢复到最新的内容版本(只恢复内容):");
        document.restoreFromMemento(caretaker.getLatestContentVersion());
        document.display();
        
        // 恢复到最新的格式版本(只恢复格式)
        System.out.println("恢复到最新的格式版本(只恢复格式):");
        document.restoreFromMemento(caretaker.getLatestFormattingVersion());
        document.display();
        
        // 恢复到最新的完整版本(恢复所有属性)
        System.out.println("恢复到最新的完整版本(恢复所有属性):");
        document.restoreFromMemento(caretaker.getLatestFullVersion());
        document.display();
    }
}

7.2 使用内部类实现

在Java中,我们可以使用内部类来实现备忘录模式,这样可以更好地保护备忘录的状态不被外部访问。

java 复制代码
// 发起人类 - 使用内部类作为备忘录
public class EditorWithInnerMemento {
    private String content;
    
    public void setContent(String content) {
        this.content = content;
    }
    
    public String getContent() {
        return content;
    }
    
    // 创建备忘录
    public Memento save() {
        return new Memento(content);
    }
    
    // 从备忘录恢复
    public void restore(Memento memento) {
        content = memento.getSavedContent();
    }
    
    // 内部类实现备忘录
    public class Memento {
        private final String savedContent;
        
        private Memento(String contentToSave) {
            savedContent = contentToSave;
        }
        
        private String getSavedContent() {
            return savedContent;
        }
    }
}

使用内部类的管理者类:

java 复制代码
// 管理者类
public class InnerMementoCaretaker {
    private List<EditorWithInnerMemento.Memento> history = new ArrayList<>();
    
    public void push(EditorWithInnerMemento.Memento memento) {
        history.add(memento);
    }
    
    public EditorWithInnerMemento.Memento pop() {
        if (history.isEmpty()) {
            return null;
        }
        EditorWithInnerMemento.Memento lastMemento = history.get(history.size() - 1);
        history.remove(history.size() - 1);
        return lastMemento;
    }
}

测试内部类实现:

java 复制代码
public class InnerMementoDemo {
    public static void main(String[] args) {
        EditorWithInnerMemento editor = new EditorWithInnerMemento();
        InnerMementoCaretaker caretaker = new InnerMementoCaretaker();
        
        // 设置初始内容
        editor.setContent("第一版内容");
        System.out.println("初始内容: " + editor.getContent());
        
        // 保存状态
        caretaker.push(editor.save());
        
        // 修改内容
        editor.setContent("第二版内容");
        System.out.println("修改后内容: " + editor.getContent());
        
        // 保存状态
        caretaker.push(editor.save());
        
        // 再次修改内容
        editor.setContent("第三版内容");
        System.out.println("再次修改后内容: " + editor.getContent());
        
        // 恢复到上一个状态
        editor.restore(caretaker.pop());
        System.out.println("恢复到上一个状态后内容: " + editor.getContent());
        
        // 再次恢复到上一个状态
        editor.restore(caretaker.pop());
        System.out.println("再次恢复后内容: " + editor.getContent());
    }
}

8. 备忘录模式在Java中的实际应用

8.1 Java中的序列化与备忘录模式

Java提供的序列化机制可以看作是备忘录模式的一种实现形式,它允许我们将对象的状态保存到文件中,并在需要时恢复。

java 复制代码
import java.io.*;

// 可序列化对象 - 相当于发起人
public class SerializableGameState implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String playerName;
    private int level;
    private int score;
    private List<String> inventory;
    
    public SerializableGameState(String playerName) {
        this.playerName = playerName;
        this.level = 1;
        this.score = 0;
        this.inventory = new ArrayList<>();
    }
    
    // 游戏进展
    public void advanceLevel() {
        level++;
        score += 1000;
    }
    
    // 增加分数
    public void addScore(int points) {
        score += points;
    }
    
    // 添加物品到库存
    public void addItemToInventory(String item) {
        inventory.add(item);
    }
    
    // 显示游戏状态
    public void displayState() {
        System.out.println("玩家: " + playerName);
        System.out.println("等级: " + level);
        System.out.println("分数: " + score);
        System.out.println("物品库存: " + inventory);
        System.out.println("--------------------");
    }
    
    // 保存状态到文件 - 使用序列化
    public void saveToFile(String fileName) {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(fileName))) {
            oos.writeObject(this);
            System.out.println("游戏状态已保存到: " + fileName);
        } catch (IOException e) {
            System.out.println("保存游戏状态失败: " + e.getMessage());
        }
    }
    
    // 从文件加载状态 - 使用反序列化
    public static SerializableGameState loadFromFile(String fileName) {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(fileName))) {
            SerializableGameState gameState = (SerializableGameState) ois.readObject();
            System.out.println("游戏状态已从: " + fileName + " 加载");
            return gameState;
        } catch (IOException | ClassNotFoundException e) {
            System.out.println("加载游戏状态失败: " + e.getMessage());
            return null;
        }
    }
}

测试序列化实现:

java 复制代码
public class SerializationMementoDemo {
    public static void main(String[] args) {
        String saveFile = "game_save.dat";
        
        // 创建新游戏
        SerializableGameState game = new SerializableGameState("玩家1");
        System.out.println("新游戏创建:");
        game.displayState();
        
        // 游戏进展
        game.advanceLevel();
        game.addScore(500);
        game.addItemToInventory("剑");
        game.addItemToInventory("盾");
        System.out.println("\n游戏进展后:");
        game.displayState();
        
        // 保存游戏状态
        game.saveToFile(saveFile);
        
        // 继续游戏
        game.advanceLevel();
        game.addScore(800);
        game.addItemToInventory("药水");
        System.out.println("\n继续游戏后:");
        game.displayState();
        
        // 加载之前保存的游戏状态
        SerializableGameState loadedGame = SerializableGameState.loadFromFile(saveFile);
        if (loadedGame != null) {
            System.out.println("\n加载存档后:");
            loadedGame.displayState();
        }
    }
}

8.2 Java Swing中的撤销/重做功能

Java Swing的UndoManager类提供了撤销/重做功能,这是备忘录模式的另一种实现。

java 复制代码
import javax.swing.*;
import javax.swing.text.*;
import javax.swing.undo.*;
import java.awt.*;
import java.awt.event.*;

public class UndoRedoTextEditor extends JFrame {
    private JTextArea textArea;
    private UndoManager undoManager;
    
    public UndoRedoTextEditor() {
        // 设置窗口
        setTitle("带撤销/重做功能的文本编辑器");
        setSize(500, 400);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        // 创建文本区域
        textArea = new JTextArea();
        JScrollPane scrollPane = new JScrollPane(textArea);
        add(scrollPane, BorderLayout.CENTER);
        
        // 创建撤销管理器
        undoManager = new UndoManager();
        
        // 监听文本变化
        Document doc = textArea.getDocument();
        doc.addUndoableEditListener(e -> undoManager.addEdit(e.getEdit()));
        
        // 创建工具栏
        JToolBar toolBar = new JToolBar();
        
        // 创建撤销按钮
        JButton undoButton = new JButton("撤销");
        undoButton.addActionListener(e -> {
            try {
                if (undoManager.canUndo()) {
                    undoManager.undo();
                }
            } catch (CannotUndoException ex) {
                System.out.println("无法撤销: " + ex);
            }
        });
        
        // 创建重做按钮
        JButton redoButton = new JButton("重做");
        redoButton.addActionListener(e -> {
            try {
                if (undoManager.canRedo()) {
                    undoManager.redo();
                }
            } catch (CannotRedoException ex) {
                System.out.println("无法重做: " + ex);
            }
        });
        
        // 添加按钮到工具栏
        toolBar.add(undoButton);
        toolBar.add(redoButton);
        
        // 添加工具栏到窗口
        add(toolBar, BorderLayout.NORTH);
    }
    
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            UndoRedoTextEditor editor = new UndoRedoTextEditor();
            editor.setVisible(true);
        });
    }
}

9. 备忘录模式与其他设计模式的结合

9.1 备忘录模式与命令模式

备忘录模式经常与命令模式结合使用,特别是在需要撤销操作的场景中。命令模式封装操作,而备忘录模式保存状态。

java 复制代码
// 命令接口
interface Command {
    void execute();
    void undo();
}

// 接收者类
class Calculator {
    private int currentValue;
    
    public Calculator() {
        this.currentValue = 0;
    }
    
    public void add(int value) {
        currentValue += value;
    }
    
    public void subtract(int value) {
        currentValue -= value;
    }
    
    public int getCurrentValue() {
        return currentValue;
    }
    
    public void setCurrentValue(int value) {
        this.currentValue = value;
    }
    
    // 备忘录内部类
    public class CalculatorMemento {
        private final int savedValue;
        
        private CalculatorMemento(int value) {
            this.savedValue = value;
        }
        
        private int getSavedValue() {
            return savedValue;
        }
    }
    
    // 创建备忘录
    public CalculatorMemento createMemento() {
        return new CalculatorMemento(currentValue);
    }
    
    // 从备忘录恢复状态
    public void restoreMemento(CalculatorMemento memento) {
        this.currentValue = memento.getSavedValue();
    }
}

// 具体命令 - 加法
class AddCommand implements Command {
    private Calculator calculator;
    private int valueToAdd;
    private Calculator.CalculatorMemento memento;
    
    public AddCommand(Calculator calculator, int valueToAdd) {
        this.calculator = calculator;
        this.valueToAdd = valueToAdd;
    }
    
    @Override
    public void execute() {
        memento = calculator.createMemento();  // 保存当前状态
        calculator.add(valueToAdd);
    }
    
    @Override
    public void undo() {
        calculator.restoreMemento(memento);    // 恢复到之前的状态
    }
}

// 具体命令 - 减法
class SubtractCommand implements Command {
    private Calculator calculator;
    private int valueToSubtract;
    private Calculator.CalculatorMemento memento;
    
    public SubtractCommand(Calculator calculator, int valueToSubtract) {
        this.calculator = calculator;
        this.valueToSubtract = valueToSubtract;
    }
    
    @Override
    public void execute() {
        memento = calculator.createMemento();  // 保存当前状态
        calculator.subtract(valueToSubtract);
    }
    
    @Override
    public void undo() {
        calculator.restoreMemento(memento);    // 恢复到之前的状态
    }
}

// 调用者 - 计算器操作管理器
class CalculatorInvoker {
    private Stack<Command> commandHistory = new Stack<>();
    
    public void executeCommand(Command command) {
        command.execute();
        commandHistory.push(command);
    }
    
    public void undoLastCommand() {
        if (!commandHistory.isEmpty()) {
            Command lastCommand = commandHistory.pop();
            lastCommand.undo();
        } else {
            System.out.println("没有可撤销的命令");
        }
    }
}

// 测试类
public class CommandMementoDemo {
    public static void main(String[] args) {
        Calculator calculator = new Calculator();
        CalculatorInvoker invoker = new CalculatorInvoker();
        
        System.out.println("初始值: " + calculator.getCurrentValue());
        
        // 执行加法命令
        Command addCommand = new AddCommand(calculator, 10);
        invoker.executeCommand(addCommand);
        System.out.println("执行加法后: " + calculator.getCurrentValue());
        
        // 执行减法命令
        Command subtractCommand = new SubtractCommand(calculator, 5);
        invoker.executeCommand(subtractCommand);
        System.out.println("执行减法后: " + calculator.getCurrentValue());
        
        // 撤销最后一个命令
        invoker.undoLastCommand();
        System.out.println("撤销后: " + calculator.getCurrentValue());
        
        // 再次撤销
        invoker.undoLastCommand();
        System.out.println("再次撤销后: " + calculator.getCurrentValue());
    }
}

10. 备忘录模式的优缺点

10.1 优点

  1. 保护封装性:备忘录模式可以保存对象的状态,同时不破坏对象的封装性。发起人对象可以决定哪些状态需要保存。

  2. 提供了可靠的恢复机制:通过备忘录模式,系统可以回到某个历史状态,提供了可靠的撤销/恢复功能。

  3. 降低发起人的复杂度:将状态存储和恢复的逻辑分离出来,发起人类不需要关心怎样保存历史状态。

  4. 支持事务操作:备忘录模式可以实现简单的事务操作,使得在操作失败时能够回滚到安全状态。

  5. 简化了发起人:发起人不需要保存自身的历史状态,也不需要管理这些历史版本。

10.2 缺点

  1. 资源消耗:如果需要保存的状态太大或者保存频率过高,可能会导致内存资源的大量占用。

  2. 管理者的膨胀:如果备忘录对象太多,管理者可能会变得非常复杂,占用大量内存。

  3. 额外的开发成本:需要为每个需要保存的对象创建相应的备忘录类。

  4. 性能问题:频繁地创建和恢复备忘录可能会影响系统性能。

  5. 可能增加复杂性:在某些情况下,增加备忘录模式可能会增加系统的复杂性,尤其是当发起人的状态非常复杂时。

11. 备忘录模式的适用场景

备忘录模式适用于以下场景:

  1. 需要提供撤销功能的应用程序:文本编辑器、图形编辑器等需要支持撤销/重做操作的应用。

  2. 需要保存历史快照的场景:如游戏存档、版本控制工具等需要保存特定时间点状态的场景。

  3. 需要事务回滚的系统:某些操作需要保证原子性,当操作失败时需要回滚到操作前的状态。

  4. 需要在不破坏封装的前提下保存和恢复对象状态:备忘录模式允许在不暴露对象内部结构的情况下操作其状态。

  5. 希望预防用户数据丢失:应用程序可以定期自动保存用户工作状态,以防意外关闭导致数据丢失。

  6. 模拟可能需要重复执行的场景:模拟实验或游戏场景,可能需要从特定节点重新开始。

12. 备忘录模式与其他模式的比较

12.1 备忘录模式 vs 原型模式

  • 备忘录模式:关注在不破坏对象封装的前提下,保存和恢复对象的状态。备忘录对象通常只存储必要的状态信息。
  • 原型模式:关注创建对象的完整副本。原型通常会复制对象的所有属性。

主要区别:

  • 备忘录模式用于保存和恢复状态,原型模式用于创建对象的副本。
  • 备忘录通常只存储必要的状态,而原型复制整个对象。

12.2 备忘录模式 vs 命令模式

  • 备忘录模式:关注对象状态的存储和恢复。
  • 命令模式:关注操作的封装,可以支持撤销/重做操作。

主要区别:

  • 备忘录保存状态,命令保存动作。
  • 命令模式通常结合备忘录模式使用,以提供基于状态的撤销功能。

12.3 备忘录模式 vs 状态模式

  • 备忘录模式:允许在不破坏封装的前提下捕获对象的内部状态,并在稍后恢复它。
  • 状态模式:允许对象在其内部状态改变时改变其行为。

主要区别:

  • 备忘录模式关注状态的存储和恢复,状态模式关注基于状态的行为变化。
  • 状态模式通常不包含恢复到之前状态的机制。

13. 备忘录模式的常见问题与解决方案

13.1 如何减少备忘录模式的内存消耗?

问题:在一些应用场景中,备忘录可能会占用大量内存资源,尤其是当需要保存大量状态或对象状态非常大时。

解决方案

  1. 增量备忘录:只保存状态的变化部分,而不是完整状态。
  2. 压缩备忘录:对备忘录中的数据进行压缩存储。
  3. 惰性保存:根据实际需要保存状态,而不是定期保存。
  4. 限制历史记录数量:设置一个最大历史记录数,超出时删除最旧的记录。
  5. 使用外部存储:将备忘录保存到文件或数据库,而不是内存中。
java 复制代码
// 增量备忘录示例
public class IncrementalMemento {
    private Map<String, Object> changedProperties = new HashMap<>();
    private IncrementalMemento previousMemento;
    
    public void setProperty(String propertyName, Object propertyValue) {
        changedProperties.put(propertyName, propertyValue);
    }
    
    public Object getProperty(String propertyName) {
        if (changedProperties.containsKey(propertyName)) {
            return changedProperties.get(propertyName);
        } else if (previousMemento != null) {
            return previousMemento.getProperty(propertyName);
        }
        return null;
    }
    
    public void setPreviousMemento(IncrementalMemento previousMemento) {
        this.previousMemento = previousMemento;
    }
}

13.2 如何实现多层撤销/重做?

问题:简单的备忘录模式通常只提供单一的撤销/恢复功能,但实际应用可能需要多层次的撤销/重做支持。

解决方案

  1. 使用两个栈:分别用于撤销和重做操作。
  2. 记录操作序列:保存完整的操作序列,支持在任意点恢复。
  3. 命令模式结合:使用命令模式记录操作,备忘录模式记录状态。
java 复制代码
// 多层撤销/重做管理器
public class MultiLevelUndoRedoManager<T> {
    private Stack<T> undoStack = new Stack<>();
    private Stack<T> redoStack = new Stack<>();
    
    public void saveState(T memento) {
        undoStack.push(memento);
        // 清空重做栈,因为有新的状态
        redoStack.clear();
    }
    
    public T undo() {
        if (undoStack.isEmpty()) {
            return null;
        }
        
        T memento = undoStack.pop();
        redoStack.push(memento);
        
        return undoStack.isEmpty() ? null : undoStack.peek();
    }
    
    public T redo() {
        if (redoStack.isEmpty()) {
            return null;
        }
        
        T memento = redoStack.pop();
        undoStack.push(memento);
        
        return memento;
    }
    
    public boolean canUndo() {
        return undoStack.size() > 1; // 至少需要两个状态才能撤销
    }
    
    public boolean canRedo() {
        return !redoStack.isEmpty();
    }
}

13.3 如何处理复杂对象的状态?

问题:当对象的状态非常复杂,包含多个相互关联的对象时,备忘录模式的实现会变得复杂。

解决方案

  1. 组合备忘录:为每个子对象创建单独的备忘录,然后组合起来。
  2. 序列化:使用Java的序列化机制来保存和恢复复杂对象状态。
  3. 选择性备忘录:只保存关键的状态信息,而不是所有状态。
  4. 分层备忘录:为对象的不同层次创建不同的备忘录。
java 复制代码
// 组合备忘录示例
public class CompositeMemento {
    private Map<String, Memento> componentMementos = new HashMap<>();
    
    public void addComponentMemento(String componentId, Memento memento) {
        componentMementos.put(componentId, memento);
    }
    
    public Memento getComponentMemento(String componentId) {
        return componentMementos.get(componentId);
    }
    
    // 通用接口
    public interface Memento {
        void restore(Object originator);
    }
}

14. 总结

备忘录模式是一种行为型设计模式,它允许在不破坏对象封装性的前提下,捕获对象的内部状态,并在需要时将对象恢复到这个状态。

14.1 核心要点

  1. 角色划分:备忘录模式包含发起人、备忘录和管理者三个核心角色,各司其职。

  2. 封装保护:备忘录模式通过限制对备忘录内部状态的访问,保证了对象的封装性。

  3. 状态恢复:备忘录模式提供了可靠的状态存储和恢复机制,支持撤销/重做等功能。

  4. 应用广泛:备忘录模式在文本编辑器、游戏存档、事务系统等多种场景中有广泛应用。

  5. 实现灵活:备忘录模式有多种实现方式,如黑箱模式、白箱模式、内部类实现等。

14.2 设计建议

  1. 轻量化备忘录:尽量只保存必要的状态信息,减小备忘录的大小。

  2. 控制备忘录数量:设置合理的备忘录保存策略,避免过多备忘录占用资源。

  3. 结合其他模式:考虑与命令模式、原型模式等结合使用,解决更复杂的问题。

  4. 注意性能问题:在频繁创建或恢复备忘录的场景中,注意性能优化。

  5. 保护隐私数据:确保备忘录中的敏感数据不会被外部访问。

备忘录模式虽然概念简单,但应用灵活,是实现状态管理、操作撤销等功能的有力工具。通过合理使用备忘录模式,我们可以使应用程序更加健壮,提供更好的用户体验。

相关推荐
D_aniel_3 分钟前
排序算法-冒泡排序
java·排序算法·冒泡排序
忘梓.1 小时前
从父类到子类:C++ 继承的奇妙旅程(1)
java·开发语言·数据库·c++
SunTecTec1 小时前
美化IDEA注释:Idea 中快捷键 Ctrl + / 自动注释的缩进(避免添加注释自动到行首)以及 Ctrl + Alt + l 全局格式化代码的注释缩进
java·ide·intellij-idea
18你磊哥2 小时前
泛型设计模式实践
设计模式
眼镜哥(with glasses)3 小时前
Java程序题案例分析
java·开发语言
纪元A梦7 小时前
华为OD机试真题——荒岛求生(2025A卷:200分)Java/python/JavaScript/C/C++/GO最佳实现
java·c语言·javascript·c++·python·华为od·go
苹果酱05677 小时前
iview 表单验证问题 Select 已经选择 还是弹验证提示
java·vue.js·spring boot·mysql·课程设计
电商数据girl8 小时前
【Python爬虫电商数据采集+数据分析】采集电商平台数据信息,并做可视化演示
java·开发语言·数据库·爬虫·python·数据分析