文章中如果有图看不到,可以点这里去 csdn 看看。从那边导过来的,文章太多,没法一篇篇修改好。

深入浅出设计模式【十八、备忘录模式】

一、备忘录模式介绍

在软件开发中,经常需要实现一些需要状态回滚的功能,例如:

  • 撤销 (Undo)重做 (Redo) 操作。
  • 游戏存档和读档。
  • 事务 (Transaction) 操作中的回滚 (Rollback)。
  • 浏览器会话恢复。

直接让外部对象访问并保存一个对象的内部状态,会严重破坏该对象的封装性,因为其内部实现细节可能会暴露给外部,使得未来对它的修改变得困难重重。

备忘录模式通过引入一个独立的“备忘录”对象来解决这个问题。该备忘录对象充当了原始对象状态的快照(Snapshot)的载体。原始对象(称为“原发器”)负责创建备忘录(保存状态)和从备忘录恢复状态。而另一个对象(称为“负责人”)则负责安全地存储备忘录,但它不能(也不应该)操作备忘录内部的内容。这样,状态保存和恢复的职责被清晰地分离,同时严格保证了原发器的封装性。

二、核心概念与意图

  1. 核心概念

    • 原发器 (Originator): 可以生成自身状态快照的对象,也可以根据快照恢复自身状态。它是需要被保存和恢复状态的那个对象。
    • 备忘录 (Memento): 存储原发器内部状态的对象。备忘录的设计要防止原发器以外的对象访问自己。通常,备忘录提供两种接口:
      • 宽接口 (Wide Interface): 供原发器使用,允许其访问所有状态,用于恢复。
      • 窄接口 (Narrow Interface): 供负责人(Caretaker)使用,它看不到备忘录的内部细节,只能存储和传递备忘录。
    • 负责人 (Caretaker): 负责保存备忘录,但不能对备忘录的内容进行操作或检查。它可以存储多个备忘录以实现多级撤销或历史记录。
  2. 意图

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

三、适用场景剖析

备忘录模式在以下场景中非常有效:

  1. 需要提供撤销 (Undo) 和重做 (Redo) 功能时: 这是备忘录模式最经典的应用场景。用户的操作可以被记录为一系列备忘录,从而实现状态的历史回溯。
  2. 需要保存和恢复对象状态,但直接暴露其状态会破坏对象的封装性时: 当对象的状态包含私有细节,不希望被其他对象直接访问时。
  3. 需要生成对象状态快照 (Snapshot) 时: 例如,游戏中的存档功能、虚拟机快照、数据库的检查点(Checkpoint)。
  4. 需要实现事务回滚时: 在业务操作失败时,可以使用备忘录将参与事务的所有对象回滚到操作前的状态。

四、UML 类图解析(Mermaid)

以下UML类图清晰地展示了备忘录模式的结构和角色间的关系,特别是其保护封装性的巧妙设计:

creates & uses
stores
Originator
-state: Object
+createMemento() : Memento
+restore(m: Memento)
Memento
-state: Object
+getSavedState() : Object
+Memento(state: Object)
Caretaker
-mementos: Stack<Memento>
+saveState(originator: Originator)
+undo(originator: Originator)
  • Originator (原发器)
    • 拥有需要保存的内部状态 (-state: Object)。
    • createMemento()创建备忘录。将当前自身状态信息复制到一个新的备忘录对象中,并返回该对象。
    • restore(m: Memento)恢复状态。传入一个备忘录对象,并将自己的状态重置为该备忘录中所保存的状态。
  • Memento (备忘录)
    • 是保存原发器状态的对象。其核心字段 (-state: Object) 通常通过构造函数设置,并且没有公共的setter方法,以保证状态 immutable(不可变)。
    • 它提供一个宽接口(如 getSavedState())给 Originator,允许其读写状态。但这个接口的可见性应设置为包级私有或通过其他方式,确保只有 Originator 能访问。
    • Caretaker 等其他类,它只暴露一个窄接口(如无任何公共方法),使其无法访问内部状态,从而保护了封装性。
  • Caretaker (负责人)
    • 负责保存备忘录,通常使用栈 (Stack) 或列表来管理多个备忘录,以支持多次撤销。
    • saveState(originator: Originator): 请求原发器创建备忘录,并将该备忘录保存到历史记录中。
    • undo(originator: Originator): 从历史记录中取出最新的备忘录,并将其交还给原发器进行状态恢复。
    • 关键Caretaker 永远不会操作或读取 Memento 的内部状态。它只负责存储和传递。

五、各种实现方式及其优缺点

备忘录模式的实现关键在于如何平衡“状态保存与恢复”和“封装性保护”。

1. 标准实现(基于“宽接口”和“窄接口”)

这是最经典的方式,通过控制接口的可见性来保护封装性。

// Memento class with restricted visibility
// This class is package-private (or a static nested class), so only Originator can access it fully.
class Memento {
    private final Object state; // Immutable state

    // Package-private constructor, only accessible within the same package (e.g., by Originator)
    Memento(Object stateToSave) {
        this.state = stateToSave; // Could be a deep copy for safety
    }

    // Package-private getter, only for Originator
    Object getSavedState() {
        return state;
    }
}

// Originator
public class Originator {
    private String state; // The state to be saved

    public void setState(String state) {
        this.state = state;
        System.out.println("State set to: " + state);
    }

    public String getState() {
        return state;
    }

    // Creates a memento, saving the current state
    public Memento createMemento() {
        return new Memento(state); // Pass internal state to memento
    }

    // Restores state from a memento
    public void restoreFromMemento(Memento memento) {
        state = (String) memento.getSavedState(); // Originator can access the wide interface
        System.out.println("State restored to: " + state);
    }
}

// Caretaker - Does NOT know the details of Memento
public class Caretaker {
    private Stack<Memento> mementoStack = new Stack<>();

    public void saveState(Originator originator) {
        Memento m = originator.createMemento();
        mementoStack.push(m);
    }

    public void undo(Originator originator) {
        if (!mementoStack.isEmpty()) {
            Memento previousMemento = mementoStack.pop();
            originator.restoreFromMemento(previousMemento);
        }
    }
}

// Client (in a different package)
public class Client {
    public static void main(String[] args) {
        Originator originator = new Originator();
        Caretaker caretaker = new Caretaker();

        originator.setState("State #1");
        caretaker.saveState(originator); // Save state

        originator.setState("State #2");
        caretaker.saveState(originator); // Save state

        originator.setState("State #3");
        System.out.println("Current State: " + originator.getState());

        caretaker.undo(originator); // Undo to State #2
        caretaker.undo(originator); // Undo to State #1
    }
}
  • 优点
    • 完美的封装性保护Memento 的内部状态对 CaretakerClient 是完全隐藏的。
    • 职责清晰: 原发器负责状态,负责人负责存储,备忘录负责承载状态。
  • 缺点
    • 可能产生大量对象: 如果状态很大或变更频繁,会创建大量备忘录对象,消耗内存。需要使用增量备份等技术优化。

2. 接口隔离实现(使用内部类)

利用Java内部类可以访问外部类私有成员的特性,实现更优雅的宽窄接口隔离。

public class Originator {
    private String state;

    public Memento createMemento() {
        return new Memento(state);
    }
    public void restore(Memento m) {
        this.state = m.getState();
    }

    // The Memento class is an inner class, giving it access to Originator's private fields.
    // But it's public, so Caretaker can see it (narrow interface).
    public class Memento {
        private final String state;

        private Memento(String state) { // Private constructor
            this.state = state;
        }
        // This method is package-private or private.
        // Only Originator can access it because it's in the same class.
        private String getState() {
            return state;
        }
    }
}
// Caretaker can hold Originator.Memento objects but cannot call getState().

3. 序列化实现

如果状态可以序列化,可以直接将原发器序列化后存储(例如存入文件、数据库)作为备忘录。恢复时再反序列化。

  • 优点
    • 实现简单,无需手动管理状态复制。
    • 易于实现持久化(存盘)。
  • 缺点
    • 序列化可能效率较低。
    • 可能暴露更多本应隐藏的字段(可通过 transient 关键字排除)。

六、最佳实践

  1. 管理内存消耗: 这是备忘录模式最大的挑战。对于状态庞大或变化频繁的对象,需谨慎使用。
    • 增量备份: 只保存相对于上一个备忘录的变化量,而不是完整状态。
    • 设定历史深度: 只保留最近N个备忘录,防止内存无限增长。
  2. 考虑持久化: 如果需要支持应用重启后的状态恢复(如游戏存档),需要将备忘录序列化到磁盘或数据库中。
  3. 与原型模式结合: 如果原发器的状态非常复杂,克隆是其内部状态的一部分,可以使用原型模式来快速创建备忘录。
  4. 明确生命周期Caretaker 只负责维护备忘录,但不应该知道其内容。确保 Memento 对象是不可变的,一旦创建就不能被修改,以保证状态的一致性。

七、在开发中的演变和应用

备忘录模式的思想是许多现代技术和架构的基础:

  1. 版本控制系统 (Git, SVN): 代码仓库的本质就是一个巨大的备忘录集合,每次提交(Commit)都是项目状态的一个备忘录。checkoutrevert 命令就是状态恢复。
  2. 数据库事务与日志: 数据库的预写日志 (WAL) 机制。在事务提交前,更改首先被记录到日志中(创建备忘录)。如果系统崩溃,数据库可以根据日志(备忘录)进行恢复(Redo)或回滚(Undo)未完成的事务。
  3. 虚拟机和容器快照: VMware、VirtualBox 和 Docker 都提供了快照功能,可以保存虚拟机或容器的某一时刻的完整状态,之后可以随时恢复。
  4. 前端状态管理 (Redux, Vuex): 这些状态管理库的核心概念之一就是“状态是不可变的”。每次状态更新都不是修改原对象,而是生成一个新的状态对象。这本质上是在连续创建备忘录。强大的“时间旅行调试”功能正是基于此实现。

八、真实开发案例(Java语言内部、知名开源框架、工具)

  1. Java Swing - UndoManager and UndoableEdit

    • Java Swing 的文本组件支持撤销/重做。其内部使用了类似备忘录模式的结构。
    • UndoableEdit 接口类似于 Memento,代表一个可撤销的操作。
    • UndoManagerCaretaker,它维护一个 UndoableEdit 的列表。
    • 文本组件(如 JTextComponent)是 Originator,它可以生成代表文本状态变化的 UndoableEdit
  2. 游戏开发 - 存档/读档

    • 游戏中的角色(Originator)拥有复杂的状态(生命值、位置、装备等)。
    • 存档时,角色对象将自己的状态序列化成一个存档文件(Memento)。
    • 读档时,从存档文件中反序列化出状态,并恢复给角色对象。
    • 游戏管理类是 Caretaker,负责管理存档文件。
  3. Spring Framework - @Transactional 与回滚

    • 虽然Spring的事务管理底层基于AOP和数据库事务,但其回滚机制的理念与备忘录模式相通
    • 在事务开始时,数据库的状态可以被视为一个隐式的“备忘录”。
    • 如果事务中发生异常,Spring会命令数据库引擎利用其日志和undo段(相当于备忘录)将数据回滚到事务开始前的状态。
  4. Java.util.Date (历史原因)

    • 早期的 java.util.Date 类有一个 getTime() 方法(返回long时间戳)和一个接受long时间戳的构造函数。这可以看作是一种最简单的备忘录模式:时间戳就是备忘录,Date 对象是原发器。

九、总结

方面总结
模式类型行为型设计模式
核心意图在不破坏封装性的前提下,捕获并外部化对象的内部状态,以便日后恢复。
关键角色原发器(Originator), 备忘录(Memento), 负责人(Caretaker)
核心机制1. 状态外部化Originator 创建 Memento 保存状态。
2. 封装保护MementoCaretaker 隐藏实现细节(窄接口)。
3. 状态恢复Originator 使用 Memento 恢复自身状态(宽接口)。
主要优点1. 完美保护封装边界,状态细节不泄露。
2. 简化原发器职责,状态恢复逻辑集中。
3. 易于实现撤销/重做、快照、事务回滚等功能。
主要缺点1. 内存消耗大: 频繁保存大状态对象开销巨大。
2. 潜在复杂度Caretaker 需管理备忘录生命周期,可能引入复杂性。
适用场景需要实现撤销/重做、事务回滚、快照/存档、状态恢复等功能,且对封装性要求高的场景。
最佳实践管理内存(增量备份、历史深度);与持久化结合;确保备忘录不可变。
现代应用版本控制系统 (Git)数据库事务与WAL虚拟机快照前端状态管理 (Redux)
真实案例Swing撤销功能 (UndoManager),游戏存档Spring事务回滚 (理念相通)。

备忘录模式是状态管理操作可逆性的强大工具。它通过巧妙的接口设计,在提供强大功能的同时,严格维护了对象的封装性原则,体现了优秀的设计思想。然而,其内存开销要求架构师在应用时必须仔细权衡,并常需结合持久化、增量备份等策略进行优化。它在从GUI到数据库,再到系统架构的众多领域中都发挥着至关重要的作用,是实现高级功能的基石模式之一。

posted @ 2025-08-30 00:20  NeoLshu  阅读(6)  评论(0)    收藏  举报  来源