公众号:【CTO成长日记】

Java 设计模式之备忘录模式:优雅保存与恢复对象状态

在软件开发中,我们经常需要保存对象的中间状态,以便在需要时能够恢复到该状态。例如文本编辑器的撤销功能、游戏中的存档机制、事务的回滚操作等。备忘录模式(Memento Pattern)正是为这类场景设计的,它提供了一种在不破坏封装性的前提下,捕获和恢复对象状态的机制。

备忘录模式的核心思想

备忘录模式属于行为型设计模式,其核心思想是在不暴露对象内部状态的情况下,捕获并保存对象的内部状态,以便后续需要时能将对象恢复到之前的状态

这种设计的好处是:

  • 保持对象状态的封装性,不暴露内部实现细节
  • 提供了状态恢复机制,支持撤销操作
  • 简化了原发器对象,将状态保存和恢复的逻辑分离出去

备忘录模式的角色组成

备忘录模式主要包含三个核心角色:

  1. 原发器(Originator):需要保存状态的对象,能创建备忘录并从备忘录恢复状态
  2. 备忘录(Memento):存储原发器的内部状态,对其他对象隐藏具体实现
  3. 负责人(Caretaker):负责管理备忘录,提供保存和获取备忘录的接口,但不操作备忘录的内容

备忘录模式的代码实现

下面我们通过一个文本编辑器的例子来实现备忘录模式,该编辑器需要支持文本内容的保存和撤销功能。

1. 定义备忘录类

备忘录类用于存储原发器的状态,为了保证封装性,它通常只允许原发器访问其内部状态,对其他对象隐藏细节。

// 备忘录类:存储文本编辑器的状态
public class EditorMemento {
    private final String content;  // 保存的文本内容
    private final int cursorPosition;  // 保存的光标位置
    
    // 构造方法私有,只能由原发器创建
    public EditorMemento(String content, int cursorPosition) {
        this.content = content;
        this.cursorPosition = cursorPosition;
    }
    
    // 只提供给原发器访问的 getter 方法
    String getContent() {
        return content;
    }
    
    int getCursorPosition() {
        return cursorPosition;
    }
}

2. 定义原发器类

原发器是需要保存状态的对象,它负责创建备忘录(保存当前状态)和从备忘录恢复状态。

// 原发器:文本编辑器
public class TextEditor {
    private String content;  // 当前文本内容
    private int cursorPosition;  // 当前光标位置
    
    public TextEditor() {
        this.content = "";
        this.cursorPosition = 0;
    }
    
    // 输入文本
    public void type(String text) {
        content = content.substring(0, cursorPosition) + text + content.substring(cursorPosition);
        cursorPosition += text.length();
    }
    
    // 移动光标
    public void moveCursor(int position) {
        if (position >= 0 && position <= content.length()) {
            this.cursorPosition = position;
        }
    }
    
    // 创建备忘录,保存当前状态
    public EditorMemento save() {
        return new EditorMemento(content, cursorPosition);
    }
    
    // 从备忘录恢复状态
    public void restore(EditorMemento memento) {
        this.content = memento.getContent();
        this.cursorPosition = memento.getCursorPosition();
    }
    
    // 辅助方法:显示当前状态
    public void showState() {
        System.out.println("文本内容:" + content);
        System.out.println("光标位置:" + cursorPosition);
        System.out.println("---------------------");
    }
    
    // getter 方法
    public String getContent() {
        return content;
    }
    
    public int getCursorPosition() {
        return cursorPosition;
    }
}

3. 定义负责人类

负责人负责管理备忘录,它可以保存多个备忘录,提供保存和获取备忘录的接口,但不能查看或修改备忘录的内容。

// 负责人:管理备忘录
import java.util.ArrayList;
import java.util.List;

public class EditorHistory {
    private List<EditorMemento> mementos = new ArrayList<>();
    
    // 保存备忘录
    public void saveMemento(EditorMemento memento) {
        mementos.add(memento);
    }
    
    // 获取上一个备忘录(撤销操作)
    public EditorMemento getPreviousMemento() {
        if (mementos.size() == 0) {
            return null;
        }
        
        // 移除并返回最后一个备忘录
        int lastIndex = mementos.size() - 1;
        EditorMemento lastMemento = mementos.get(lastIndex);
        mementos.remove(lastIndex);
        return lastMemento;
    }
}

4. 客户端使用示例

public class Client {
    public static void main(String[] args) {
        // 创建文本编辑器和历史记录管理器
        TextEditor editor = new TextEditor();
        EditorHistory history = new EditorHistory();
        
        // 第一次操作
        editor.type("Hello, ");
        history.saveMemento(editor.save());  // 保存状态
        System.out.println("第一次操作后:");
        editor.showState();
        
        // 第二次操作
        editor.type("World!");
        history.saveMemento(editor.save());  // 保存状态
        System.out.println("第二次操作后:");
        editor.showState();
        
        // 第三次操作
        editor.moveCursor(7);
        editor.type("Java ");
        history.saveMemento(editor.save());  // 保存状态
        System.out.println("第三次操作后:");
        editor.showState();
        
        // 第一次撤销
        editor.restore(history.getPreviousMemento());
        System.out.println("第一次撤销后:");
        editor.showState();
        
        // 第二次撤销
        editor.restore(history.getPreviousMemento());
        System.out.println("第二次撤销后:");
        editor.showState();
    }
}

运行结果:

第一次操作后:
文本内容:Hello, 
光标位置:7
---------------------
第二次操作后:
文本内容:Hello, World!
光标位置:13
---------------------
第三次操作后:
文本内容:Hello, Java World!
光标位置:12
---------------------
第一次撤销后:
文本内容:Hello, World!
光标位置:13
---------------------
第二次撤销后:
文本内容:Hello, 
光标位置:7
---------------------

Java 中的备忘录模式应用

在 Java 标准库和常见框架中,备忘录模式的思想也有广泛应用:

  1. java.util.DateDate类的clone()方法可以创建对象的副本,本质上是备忘录的一种简化实现
  2. java.io.Serializable:通过序列化可以保存对象的完整状态,需要时再反序列化恢复,这是一种广义的备忘录实现
  3. Spring 的事务管理:Spring 的事务回滚机制本质上使用了备忘录模式的思想,保存事务开始前的状态,以便在需要时恢复
  4. 编辑器和 IDE 的撤销功能:几乎所有文本编辑器和 IDE 都实现了撤销 / 重做功能,底层通常使用备忘录模式

备忘录模式的变种

根据实际需求,备忘录模式有几种常见变种:

  1. 增量备忘录:只记录与上一个状态的差异,而不是完整状态,适用于状态较大的场景
  2. 带时间戳的备忘录:为每个备忘录添加时间戳,支持按时间点恢复
  3. 限制备忘录数量:当备忘录数量达到上限时,移除最早的备忘录,适用于内存有限的场景
  4. 可重做的备忘录:维护两个栈(撤销栈和重做栈),支持撤销和重做操作

备忘录模式的适用场景

  • 当需要保存和恢复对象的中间状态(如撤销操作)时
  • 当不希望暴露对象的内部状态,同时又需要获取其状态时
  • 当对象的状态变化频繁,且需要记录状态变化历史时
  • 当需要实现快照功能,以便后续可以回到指定时间点的状态时

备忘录模式的优缺点

优点:

  • 保持了对象状态的封装性,避免了暴露内部实现细节
  • 提供了状态恢复机制,支持灵活的撤销操作
  • 简化了原发器的设计,将状态管理逻辑分离出去
  • 便于维护状态历史,支持多步撤销

缺点:

  • 如果对象状态较大,频繁创建备忘录会消耗较多内存
  • 负责人需要管理备忘录的生命周期,增加了系统复杂度
  • 如果对象状态包含不可序列化的资源,备忘录的实现会变得复杂

总结

备忘录模式通过分离对象状态的保存和恢复逻辑,在不破坏封装性的前提下,为对象提供了灵活的状态管理机制。它特别适合需要实现撤销功能、状态快照或事务回滚的场景。

在实际开发中,我们需要根据对象状态的大小和变化频率,权衡内存消耗和功能需求,选择合适的备忘录实现方式。理解备忘录模式的设计思想,有助于我们设计出更灵活、更健壮的状态管理机制。

原文:CTO成长日记

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐