零基础设计模式——行为型模式 - 备忘录模式
第四部分:行为型模式 - 备忘录模式 (Memento Pattern)
接下来,我们学习备忘录模式。这个模式在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
- 核心思想:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。
备忘录模式 (Memento Pattern)
“在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。” (Without violating encapsulation, capture and externalize an object’s internal state so that the object can be restored to this state later.)
想象一下你在玩一个电子游戏,游戏允许你随时“存档”(Save Game):
- 你当前的游戏进度 (Originator’s State):包括你的角色位置、等级、拥有的物品、任务完成情况等。
- 游戏存档文件 (Memento):当你点击“存档”时,游戏系统会把所有这些当前状态信息打包成一个存档文件。这个存档文件就是备忘录。它对你(玩家)来说可能是一个黑盒子,你不能直接修改里面的具体数据,但游戏系统知道如何读取它。
- 游戏系统/存档管理器 (Caretaker):负责保存这些存档文件(备忘录)。它可以保存多个存档,让你选择加载哪个。
- 游戏本身 (Originator):是状态的拥有者。它知道如何根据存档文件恢复自己的状态(“读档” Load Game),也知道如何创建包含当前状态的存档文件。
关键在于,存档文件(备忘录)对玩家(或存档管理器)隐藏了游戏进度的具体内部结构,保护了封装性。只有游戏本身(Originator)才能理解和使用备忘录中的数据来恢复状态。
1. 目的 (Intent)
备忘录模式的主要目的:
- 捕获和外部化对象状态:允许在不暴露对象内部实现细节的前提下,保存对象的某个时间点的状态。
- 恢复对象状态:允许对象在将来某个时刻恢复到之前保存的某个状态。
- 保持封装性:备忘录对象对外部(除了其创建者 Originator)隐藏了状态的具体内容和结构。
常用于实现撤销/重做 (Undo/Redo) 功能、事务回滚、快照等。
2. 生活中的例子 (Real-world Analogy)
-
文本编辑器的撤销/重做功能:
- 编辑器中的文本内容和光标位置 (Originator’s State)。
- 每次重要操作(如输入、删除、格式更改)后,编辑器可能会创建一个状态快照 (Memento)。
- 这些快照被一个历史记录管理器 (Caretaker) 保存。
- 当你点击“撤销”时,编辑器 (Originator) 从历史记录管理器中获取上一个备忘录,并用它来恢复文本状态。
-
浏览器的“后退”按钮:
- 当前网页的状态 (Originator’s State):URL、滚动位置、表单数据等。
- 每次访问新页面,浏览器可能会将前一个页面的状态保存为一个历史条目 (Memento)。
- 历史记录 (Caretaker) 管理这些条目。
- 点击“后退”时,浏览器从历史记录中取出上一个备忘录并恢复页面状态。
-
数据库事务的回滚:
- 在事务开始前,数据库系统的状态或将要修改的数据 (Originator’s State) 可以被记录下来 (Memento)。
- 如果事务失败,可以使用备忘录将系统恢复到事务开始前的状态。
3. 结构 (Structure)
备忘录模式通常包含以下三个主要角色:
-
Originator (原发器/发起人):
- 创建一个包含其当前内部状态快照的备忘录对象 (
createMemento()
)。 - 使用备忘录对象来恢复其内部状态 (
restoreFromMemento(memento)
)。 - 原发器是唯一知道备忘录内部结构的对象。备忘录对原发器来说是“白盒”的。
- 创建一个包含其当前内部状态快照的备忘录对象 (
-
Memento (备忘录):
- 存储原发器对象的内部状态。原发器根据需要决定备忘录存储哪些状态信息。
- 防止原发器以外的其他对象访问备忘录。备忘录对其他对象(包括负责人 Caretaker)来说是“黑盒”的,它们只能传递备忘录,不能修改或查看其内容。
- 通常备忘录有两个接口:
- 一个宽接口给原发器:允许原发器访问恢复其状态所需的所有数据。这个接口通常只有原发器可见(例如,在Java中,Memento可以是Originator的内部类,或者Memento的方法是包级私有的,并且Originator和Memento在同一个包中)。
- 一个窄接口给负责人:负责人只能看到备忘录的标记接口或者非常有限的方法(例如,可能只有获取时间戳或描述的方法),但不能访问备忘录中存储的状态数据。
-
Caretaker (负责人/管理者):
- 负责保存好备忘录。它不知道备忘录的内部状态,也不操作备忘录的内容。
- 它只负责存储和传递备忘录对象。
- 可以存储一个或多个备忘录,例如在一个栈中以支持多次撤销。
封装性技巧:
- Java/C#: Memento 可以是 Originator 的内部类(inner class / nested class)。这样 Memento 可以访问 Originator 的私有成员,而 Originator 可以访问 Memento 的(可能是私有的)状态获取/设置方法。对外部(如 Caretaker),Memento 只暴露一个标记接口或无操作的公共接口。
- Go: 由于没有严格的私有内部类概念,可以通过接口来实现窄接口。Memento 结构体可以是不导出的(包私有),或者其状态字段不导出。Originator 和 Memento 在同一个包中,Originator 可以访问 Memento 的内部。Caretaker 通过一个只定义了空方法的接口(或不定义方法,仅作标记)来持有 Memento,或者 Memento 自身不暴露获取状态的方法给外部包。
4. 适用场景 (When to Use)
- 当需要保存一个对象在某一个时刻的(部分或全部)状态,以便以后可以恢复到这个状态时。
- 当用一个接口来让其他对象得到这些状态,会暴露对象的实现细节并破坏对象的封装性时。
- 实现撤销/重做 (Undo/Redo) 功能。
- 实现数据库或事务的快照和回滚。
- 在需要进行“试探性”操作,如果失败则回滚到操作前状态的场景。
5. 优缺点 (Pros and Cons)
优点:
- 保持了封装性:原发器的状态被封装在备忘录中,对负责人是透明的,不会暴露原发器的内部实现细节。
- 简化了原发器:原发器不需要管理其历史状态版本,这些职责转移给了负责人和备忘录。
- 高内聚,低耦合:原发器和备忘录紧密相关,但与负责人是松耦合的。
- 提供了状态恢复机制:能够方便地将对象恢复到之前的某个状态。
缺点:
- 资源消耗:如果原发器的状态非常大,或者需要频繁地创建备忘录,可能会导致较大的内存消耗和性能开销。
- 负责人需要管理备忘录的生命周期:如果备忘录没有被正确管理(例如,忘记释放不再需要的备忘录),可能导致内存泄漏。
- 实现可能复杂:尤其是在需要严格控制备忘录访问权限以保护封装性时,不同语言的实现技巧可能有所不同。
6. 实现方式 (Implementations)
让我们以一个简单的文本编辑器 (TextEditor) 为例,它可以保存和恢复文本内容。
备忘录 (EditorMemento - Memento)
// editor_memento.go (Memento)
package memento// EditorMemento 存储 Editor 的状态
// 这个结构体本身可以是不导出的,或者其字段不导出,以限制外部访问
// Originator (Editor) 在同一个包中,可以直接访问。
type editorMemento struct { // 小写开头,包私有content string
}// Memento 是一个空接口,用于 Caretaker 持有,隐藏内部结构
// 或者可以是一个包含元数据(如时间戳)的接口
type Memento interface {// GetDescription() string // 可选,给 Caretaker 一些描述信息
}// editorMemento 实现了 Memento 接口 (隐式,因为 Memento 是空接口,任何类型都满足)
// 如果 Memento 接口有方法,editorMemento 需要实现它们。// newEditorMemento 创建备忘录,只能由 Originator (Editor) 调用
// 因此,这个函数也应该是包私有的,或者 Editor 直接创建 editorMemento 实例
func newEditorMemento(content string) *editorMemento { // 包私有构造函数return &editorMemento{content: content}
}// getContent 获取状态,只能由 Originator (Editor) 调用
func (m *editorMemento) getContent() string { // 包私有方法return m.content
}
// EditorMemento.java (Memento)
package com.example.memento;// Memento interface (often a marker interface, or with limited methods for Caretaker)
// For simplicity, we can make EditorMemento directly usable by Caretaker if we trust it,
// or use a stricter approach.
// A common strict approach is to have Memento as an interface, and the concrete memento
// class is package-private or an inner class of Originator.public class EditorMemento {// The state is kept package-private or private, accessible only by Originatorprivate final String content; // State to be saved// Constructor is package-private, so only Originator (in the same package) can create it.EditorMemento(String content) {this.content = content;}// Getter is package-private, so only Originator can access the state.String getContent() {return content;}// Optionally, provide a public method for Caretaker if it needs some metadata// public String getTimestamp() { ... }
}
原发器 (TextEditor - Originator)
// text_editor.go (Originator)
package memento // 同一个包import "fmt"// TextEditor 是原发器
type TextEditor struct {content string
}func NewTextEditor() *TextEditor {return &TextEditor{content: ""}
}func (e *TextEditor) Type(words string) {e.content += wordsfmt.Printf("Editor typed: %s. Current content: '%s'\n", words, e.content)
}func (e *TextEditor) GetContent() string {return e.content
}// CreateMemento 创建备忘录
func (e *TextEditor) CreateMemento() Memento { // 返回接口类型fmt.Printf("Editor: Creating Memento with content: '%s'\n", e.content)return newEditorMemento(e.content) // 调用包私有的构造函数
}// RestoreFromMemento 从备忘录恢复状态
func (e *TextEditor) RestoreFromMemento(m Memento) {// 需要类型断言来访问具体备忘录的方法em, ok := m.(*editorMemento) // 断言为具体的 editorMemento 类型if !ok {fmt.Println("Editor: Failed to restore. Invalid Memento type.")return}e.content = em.getContent() // 调用包私有的 getContentfmt.Printf("Editor: Restored content from Memento: '%s'\n", e.content)
}
// TextEditor.java (Originator)
package com.example.memento; // Same package as EditorMementopublic class TextEditor {private String content; // The state of the editorpublic TextEditor() {this.content = "";}public void type(String words) {this.content += words;System.out.println("Editor typed: " + words + ". Current content: '" + content + "'");}public String getContent() {return content;}// Creates a Memento containing a snapshot of the editor's current statepublic EditorMemento createMemento() {System.out.println("Editor: Creating Memento with content: '" + content + "'");return new EditorMemento(this.content); // Calls package-private constructor}// Restores the editor's state from a Memento objectpublic void restoreFromMemento(EditorMemento memento) {if (memento != null) {this.content = memento.getContent(); // Calls package-private getterSystem.out.println("Editor: Restored content from Memento: '" + content + "'");} else {System.out.println("Editor: Cannot restore from a null memento.");}}
}
负责人 (History/Caretaker)
// history.go (Caretaker)
package caretaker // 可以是不同的包,也可以是 memento 包的一部分import ("../memento" // 导入 memento 包以使用 Memento 接口"fmt"
)// History 是负责人,管理备忘录列表
type History struct {mementos []memento.Memento // 持有 Memento 接口类型的列表
}func NewHistory() *History {return &History{mementos: make([]memento.Memento, 0)}
}func (h *History) AddMemento(m memento.Memento) {fmt.Println("History: Adding Memento.")h.mementos = append(h.mementos, m)
}func (h *History) GetMemento(index int) memento.Memento {if index < 0 || index >= len(h.mementos) {fmt.Println("History: Invalid index for Memento.")return nil}fmt.Printf("History: Retrieving Memento at index %d.\n", index)return h.mementos[index]
}// GetLastMemento 通常用于撤销
func (h *History) GetLastMemento() memento.Memento {if len(h.mementos) == 0 {fmt.Println("History: No Mementos to retrieve.")return nil}lastIndex := len(h.mementos) - 1m := h.mementos[lastIndex]h.mementos = h.mementos[:lastIndex] // 弹出最后一个fmt.Printf("History: Retrieving and removing last Memento.\n")return m
}
// History.java (Caretaker)
package com.example.caretaker; // Different package from mementoimport com.example.memento.EditorMemento; // Caretaker knows about Memento type
import java.util.Stack;public class History {private Stack<EditorMemento> mementos = new Stack<>();public void addMemento(EditorMemento memento) {System.out.println("History: Adding Memento.");mementos.push(memento);}// Gets the most recent Memento (for undo) and removes it from historypublic EditorMemento getLastMemento() {if (mementos.isEmpty()) {System.out.println("History: No Mementos to retrieve.");return null;}System.out.println("History: Retrieving and removing last Memento.");return mementos.pop();}// Gets a Memento at a specific point without removing (less common for simple undo)public EditorMemento getMemento(int index) {if (index < 0 || index >= mementos.size()) {System.out.println("History: Invalid index for Memento.");return null;}// Note: Stack.get(index) is possible but pop/push is more typical for undo stackreturn mementos.get(index); // This might not be what you want for a strict undo stack}
}
客户端使用
// main.go (示例用法)
/*
package mainimport ("./caretaker""./memento""fmt"
)func main() {editor := memento.NewTextEditor()history := caretaker.NewHistory()// 初始状态fmt.Println("Initial content:", editor.GetContent()) // ''// 操作1editor.Type("Hello, ")history.AddMemento(editor.CreateMemento()) // 保存状态1: 'Hello, '// 操作2editor.Type("World!")history.AddMemento(editor.CreateMemento()) // 保存状态2: 'Hello, World!'fmt.Println("Current content:", editor.GetContent()) // 'Hello, World!'// 操作3editor.Type(" How are you?")// 不保存这个状态fmt.Println("Current content after unsaved change:", editor.GetContent()) // 'Hello, World! How are you?'// 撤销到状态2fmt.Println("\n--- Undoing last saved state (to 'Hello, World!') ---")// 注意:我们的 GetLastMemento 会移除备忘录。如果只想获取而不移除,需要不同方法。// 这里我们假设撤销就是回到上一个保存点,并且那个保存点从历史中移除(像栈一样)memento2 := history.GetLastMemento() // 获取并移除 'Hello, World!' 的备忘录if memento2 != nil {editor.RestoreFromMemento(memento2)}fmt.Println("Content after first undo:", editor.GetContent()) // 'Hello, World!'// 再次撤销到状态1fmt.Println("\n--- Undoing to previous saved state (to 'Hello, ') ---")memento1 := history.GetLastMemento() // 获取并移除 'Hello, ' 的备忘录if memento1 != nil {editor.RestoreFromMemento(memento1)}fmt.Println("Content after second undo:", editor.GetContent()) // 'Hello, '// 尝试再次撤销,历史记录已空fmt.Println("\n--- Attempting undo on empty history ---")emptyMemento := history.GetLastMemento()if emptyMemento == nil {fmt.Println("History is empty, cannot undo further.")}fmt.Println("Final content:", editor.GetContent()) // 'Hello, '
}
*/
// Main.java (示例用法)
/*
package com.example;import com.example.caretaker.History;
import com.example.memento.EditorMemento;
import com.example.memento.TextEditor;public class Main {public static void main(String[] args) {TextEditor editor = new TextEditor();History history = new History();// Initial stateSystem.out.println("Initial content: '" + editor.getContent() + "'");// Operation 1editor.type("This is the first sentence. ");history.addMemento(editor.createMemento()); // Save state 1// Operation 2editor.type("This is the second sentence. ");history.addMemento(editor.createMemento()); // Save state 2System.out.println("Current content: '" + editor.getContent() + "'");// Operation 3 (unsaved)editor.type("And this is an unsaved change.");System.out.println("Current content after unsaved change: '" + editor.getContent() + "'");// Undo to state 2System.out.println("\n--- Undoing to last saved state (State 2) ---");EditorMemento memento2 = history.getLastMemento(); // Gets and removes State 2's mementoeditor.restoreFromMemento(memento2);System.out.println("Content after first undo: '" + editor.getContent() + "'");// Undo to state 1System.out.println("\n--- Undoing to previous saved state (State 1) ---");EditorMemento memento1 = history.getLastMemento(); // Gets and removes State 1's mementoeditor.restoreFromMemento(memento1);System.out.println("Content after second undo: '" + editor.getContent() + "'");// Attempt to undo again (history is empty)System.out.println("\n--- Attempting undo on empty history ---");EditorMemento emptyMemento = history.getLastMemento();if (emptyMemento == null) {System.out.println("History is empty, cannot undo further.");}// editor.restoreFromMemento(emptyMemento); // This would try to restore from nullSystem.out.println("Final content: '" + editor.getContent() + "'");}
}
*/
7. 总结
备忘录模式提供了一种优雅的方式来捕获和恢复对象的状态,同时不破坏其封装性。它将状态存储的责任从原发器中分离出来,交由备忘录对象和负责人对象管理。这种模式是实现撤销/重做、事务回滚和系统快照等功能的关键技术。虽然需要注意潜在的资源消耗问题,但其在增强系统鲁棒性和用户体验方面的价值是显著的。