Go语言设计模式:备忘录模式详解
文章目录
- 一、备忘录模式概述
 - 1.1 备忘录模式介绍
 - 1.2 模式核心概念
 - 1.3 UML 类图
 - 1.4 备忘录模式优缺点分析
 
- 二、Go语言实现备忘录模式
 - 2.1 步骤 1: 定义备忘录
 - 2.2 步骤 2: 实现发起人
 - 2.3 步骤 3: 实现负责人
 - 2.4 步骤 4: 客户端代码与演示
 - 2.5 Go语言中的变体:使用序列化
 - 2.6 完整代码
 - 2.7 执行结果和分析
 
一、备忘录模式概述
1.1 备忘录模式介绍
备忘录模式是一种行为设计模式,它允许在不破坏对象封装性的前提下,捕获并保存一个对象的内部状态,以便在将来可以将该对象恢复到这个保存的状态。
想象一下游戏中的存档功能。你可以在任何时候保存游戏(创建备忘录),如果之后操作失误,可以读取之前的存档(恢复状态),而不需要知道游戏内部是如何保存和恢复所有角色、场景数据的。
1.2 模式核心概念
备忘录模式主要包含三个核心角色:
- Originator (发起人): 
- 需要被保存状态的对象。
 - 它创建一个包含其当前内部状态的备忘录对象。
 - 它也可以使用备忘录对象来恢复其内部状态。
 
 - Memento (备忘录): 
- 用于存储发起人的内部状态。
 - 备忘录的设计通常是其状态对外部不可见,只有发起人可以访问其内部信息。这保证了封装性。
 
 - Caretaker (负责人/管理者): 
- 负责保存备忘录对象。
 - 它从不检查或操作备忘录的内容,它只是像一个“保险箱”一样,负责存储和提供备忘录。
 
 
1.3 UML 类图
1.4 备忘录模式优缺点分析
优点:
- 封装性得到保护:备忘录将发起人的内部状态封装起来,负责人无法看到或修改它,只有发起人自己能访问。
 - 简化了发起人:发起人不需要自己管理历史版本,它只负责创建和恢复备忘录,将管理的职责交给了负责人。
 - 提供了状态回滚的机制:可以轻松实现撤销、恢复、快照等功能。
 
缺点:
- 资源消耗:如果发起人的状态很大,或者需要保存的历史版本很多,备忘录会占用大量内存。
 - 维护成本:负责人需要存储所有的备忘录,如果备忘录对象本身很复杂,维护成本会很高。
 - 实现可能很复杂:对于某些语言,保证备忘录的不可变性或封装性需要额外技巧。在Go中,通过包内可见性可以很好地解决。
 
二、Go语言实现备忘录模式
在Go语言中,我们可以通过结构体和接口的组合来实现备忘录模式。一个关键点是如何实现备忘录的“不可见性”。Go没有 private 构造函数,但我们可以通过将备忘录结构体定义在同一个包中,并将其字段设为小写(未导出),来限制外部包的访问。
我们通过一个文本编辑器的例子来演示:用户可以输入文本,随时保存快照,并能撤销到任何一个历史版本。
2.1 步骤 1: 定义备忘录
备忘录通常是一个简单的数据容器,用于存储状态。
// memento.go
package main
// TextEditorMemento 文本编辑器的备忘录
// 注意:state字段是小写的,意味着它只能在当前包内被访问,从而保护了封装性。
type TextEditorMemento struct {state string
}
// getState 是一个包内方法,供发起人使用
func (m *TextEditorMemento) getState() string {return m.state
}
 
2.2 步骤 2: 实现发起人
TextEditor 是发起人,它拥有状态(文本内容),并能创建和恢复备忘录。
// originator.go
package main
import "fmt"
// TextEditor 发起人:文本编辑器
type TextEditor struct {text string
}
// NewTextEditor 创建一个新的文本编辑器
func NewTextEditor() *TextEditor {return &TextEditor{text: ""}
}
// Write 写入文本,改变状态
func (e *TextEditor) Write(text string) {e.text += text
}
// GetContent 获取当前内容
func (e *TextEditor) GetContent() string {return e.text
}
// Save 创建一个备忘录,保存当前状态
func (e *TextEditor) Save() *TextEditorMemento {fmt.Printf("  [发起人] 保存状态: \"%s\"\n", e.text)return &TextEditorMemento{state: e.text}
}
// Restore 从备忘录恢复状态
func (e *TextEditor) Restore(memento *TextEditorMemento) {e.text = memento.getState()fmt.Printf("  [发起人] 恢复状态到: \"%s\"\n", e.text)
}
 
2.3 步骤 3: 实现负责人
History 是负责人,它负责管理备忘录的栈,实现撤销功能。
// caretaker.go
package main
import "fmt"
// History 负责人:历史记录管理器
type History struct {mementos []*TextEditorMemento
}
// NewHistory 创建一个新的历史记录
func NewHistory() *History {return &History{mementos: make([]*TextEditorMemento, 0)}
}
// Backup 保存一个备忘录
func (h *History) Backup(memento *TextEditorMemento) {h.mementos = append(h.mementos, memento)fmt.Printf("  [负责人] 备份已保存。当前历史记录数: %d\n", len(h.mementos))
}
// Undo 撤销到上一个状态
func (h *History) Undo() *TextEditorMemento {if len(h.mementos) == 0 {fmt.Println("  [负责人] 没有可撤销的历史记录。")return nil}// 获取最后一个备忘录lastIndex := len(h.mementos) - 1memento := h.mementos[lastIndex]// 从历史中移除它h.mementos = h.mementos[:lastIndex]fmt.Printf("  [负责人] 执行撤销。当前历史记录数: %d\n", len(h.mementos))return memento
}
 
2.4 步骤 4: 客户端代码与演示
现在,我们把所有部分组合起来,模拟用户在编辑器中输入、保存和撤销的过程。
// main.go
package main
func main() {editor := NewTextEditor()history := NewHistory()fmt.Println("--- 开始编辑 ---")editor.Write("Hello, ")fmt.Println("当前内容:", editor.GetContent())history.Backup(editor.Save()) // 第一次保存editor.Write("World!")fmt.Println("当前内容:", editor.GetContent())history.Backup(editor.Save()) // 第二次保存editor.Write(" Go is awesome.")fmt.Println("当前内容:", editor.GetContent())history.Backup(editor.Save()) // 第三次保存fmt.Println("\n--- 开始撤销 ---")// 撤销到第三个状态memento := history.Undo()if memento != nil {editor.Restore(memento)fmt.Println("撤销后内容:", editor.GetContent())}// 再次撤销memento = history.Undo()if memento != nil {editor.Restore(memento)fmt.Println("再次撤销后内容:", editor.GetContent())}// 再次撤销memento = history.Undo()if memento != nil {editor.Restore(memento)fmt.Println("再次撤销后内容:", editor.GetContent())}// 尝试在空历史记录上撤销memento = history.Undo()if memento != nil {editor.Restore(memento)fmt.Println("再次撤销后内容:", editor.GetContent())}
}
 
2.5 Go语言中的变体:使用序列化
当对象状态非常复杂时,手动创建备忘录结构体会很繁琐。一个更通用的方法是使用序列化(如 json 或 gob)将整个对象的状态转换成字节流(字符串),这就是备忘录。恢复时再反序列化回去。
 这种方式下,Memento 可以简单地是一个 []byte 或 string。
简化的序列化版本:
import ("encoding/json""fmt"
)
// 发起人增加序列化方法
func (e *TextEditor) SaveToString() (string, error) {data, err := json.Marshal(e)if err != nil {return "", err}return string(data), nil
}
func (e *TextEditor) RestoreFromString(data string) error {// 注意:这里为了简化,直接覆盖了e,实际中可能需要更精细的字段赋值var newEditor TextEditorerr := json.Unmarshal([]byte(data), &newEditor)if err == nil {e.text = newEditor.text}return err
}
// 负责人管理字符串切片
type StringHistory struct {states []string
}
func (h *StringHistory) Backup(state string) {h.states = append(h.states, state)
}
func (h *StringHistory) Undo() string {if len(h.states) == 0 {return ""}lastIndex := len(h.states) - 1state := h.states[lastIndex]h.states = h.states[:lastIndex]return state
}
 
这种方法的优点是通用性强,缺点是性能开销比直接内存拷贝大,且需要确保对象的所有字段都是可序列化的。
2.6 完整代码
为了方便运行,将所有代码都放在了一个 main.go 文件中。代码结构遵循了之前讲解的三个角色:发起人、备忘录和负责人。
// main.go
package main
import "fmt"
// ==================== 1. 备忘录 ====================
// TextEditorMemento 文本编辑器的备忘录
// 注意:state字段是小写的,意味着它只能在当前包内被访问,从而保护了封装性。
type TextEditorMemento struct {state string
}
// getState 是一个包内方法,供发起人使用
func (m *TextEditorMemento) getState() string {return m.state
}
// ==================== 2. 发起人 ====================
// TextEditor 发起人:文本编辑器
type TextEditor struct {text string
}
// NewTextEditor 创建一个新的文本编辑器
func NewTextEditor() *TextEditor {return &TextEditor{text: ""}
}
// Write 写入文本,改变状态
func (e *TextEditor) Write(text string) {e.text += text
}
// GetContent 获取当前内容
func (e *TextEditor) GetContent() string {return e.text
}
// Save 创建一个备忘录,保存当前状态
func (e *TextEditor) Save() *TextEditorMemento {fmt.Printf("  [发起人] 保存状态: \"%s\"\n", e.text)return &TextEditorMemento{state: e.text}
}
// Restore 从备忘录恢复状态
func (e *TextEditor) Restore(memento *TextEditorMemento) {e.text = memento.getState()fmt.Printf("  [发起人] 恢复状态到: \"%s\"\n", e.text)
}
// ==================== 3. 负责人 ====================
// History 负责人:历史记录管理器
type History struct {mementos []*TextEditorMemento
}
// NewHistory 创建一个新的历史记录
func NewHistory() *History {return &History{mementos: make([]*TextEditorMemento, 0)}
}
// Backup 保存一个备忘录
func (h *History) Backup(memento *TextEditorMemento) {h.mementos = append(h.mementos, memento)fmt.Printf("  [负责人] 备份已保存。当前历史记录数: %d\n", len(h.mementos))
}
// Undo 撤销到上一个状态
func (h *History) Undo() *TextEditorMemento {if len(h.mementos) == 0 {fmt.Println("  [负责人] 没有可撤销的历史记录。")return nil}// 获取最后一个备忘录lastIndex := len(h.mementos) - 1memento := h.mementos[lastIndex]// 从历史中移除它h.mementos = h.mementos[:lastIndex]fmt.Printf("  [负责人] 执行撤销。当前历史记录数: %d\n", len(h.mementos))return memento
}
// ==================== 4. 客户端代码与演示 ====================
func main() {editor := NewTextEditor()history := NewHistory()fmt.Println("--- 开始编辑 ---")editor.Write("Hello, ")fmt.Println("当前内容:", editor.GetContent())history.Backup(editor.Save()) // 第一次保存editor.Write("World!")fmt.Println("当前内容:", editor.GetContent())history.Backup(editor.Save()) // 第二次保存editor.Write(" Go is awesome.")fmt.Println("当前内容:", editor.GetContent())history.Backup(editor.Save()) // 第三次保存fmt.Println("\n--- 开始撤销 ---")// 撤销到第三个状态memento := history.Undo()if memento != nil {editor.Restore(memento)fmt.Println("撤销后内容:", editor.GetContent())}// 再次撤销memento = history.Undo()if memento != nil {editor.Restore(memento)fmt.Println("再次撤销后内容:", editor.GetContent())}// 再次撤销memento = history.Undo()if memento != nil {editor.Restore(memento)fmt.Println("再次撤销后内容:", editor.GetContent())}// 尝试在空历史记录上撤销fmt.Println("\n--- 尝试在空历史记录上撤销 ---")memento = history.Undo()if memento != nil {editor.Restore(memento)fmt.Println("再次撤销后内容:", editor.GetContent())}
}
 
2.7 执行结果和分析
程序运行后,你将在终端看到以下输出:
--- 开始编辑 ---
当前内容: Hello, [发起人] 保存状态: "Hello, "[负责人] 备份已保存。当前历史记录数: 1
当前内容: Hello, World![发起人] 保存状态: "Hello, World!"[负责人] 备份已保存。当前历史记录数: 2
当前内容: Hello, World! Go is awesome.[发起人] 保存状态: "Hello, World! Go is awesome."[负责人] 备份已保存。当前历史记录数: 3
--- 开始撤销 ---[负责人] 执行撤销。当前历史记录数: 2[发起人] 恢复状态到: "Hello, World! Go is awesome."
撤销后内容: Hello, World! Go is awesome.[负责人] 执行撤销。当前历史记录数: 1[发起人] 恢复状态到: "Hello, World!"
再次撤销后内容: Hello, World![负责人] 执行撤销。当前历史记录数: 0[发起人] 恢复状态到: "Hello, "
再次撤销后内容: Hello, 
--- 尝试在空历史记录上撤销 ---[负责人] 没有可撤销的历史记录。
 
结果分析:
- 编辑与备份阶段: 
- 用户每次输入文本后,
editor.Save()都会创建一个包含当前状态的TextEditorMemento对象。 history.Backup()将这个备忘录对象存入一个历史记录栈中。- 这个过程清晰地展示了发起人创建状态,负责人保存状态的职责分离。
 
 - 用户每次输入文本后,
 - 撤销阶段: 
- 每次调用 
history.Undo(),负责人都会从栈顶弹出一个备忘录,并将其返回。 editor.Restore()接收到这个备忘录后,使用其内部状态来恢复自己的text字段。- 编辑器的内容成功地回退到了上一个保存的版本。
 
 - 每次调用 
 - 边界情况: 
- 当历史记录为空时,再次调用 
Undo(),负责人会友好地提示“没有可撤销的历史记录”,并返回nil,避免了程序错误。 
 - 当历史记录为空时,再次调用 
 
这个例子完美地演示了备忘录模式如何在不暴露 TextEditor 内部实现的情况下,实现了状态的保存和恢复功能。
总结:备忘录模式是Go中实现状态快照和撤销功能的理想选择。
- 对于状态简单、性能要求高的场景,使用结构体作为备忘录是最佳选择,利用Go的包内可见性来保证封装。
 - 对于状态复杂、需要通用性的场景,使用序列化作为备忘录的实现方式更加灵活。
 
理解备忘录模式的关键在于分清三个角色的职责:发起人只关心自己的业务和状态的创建/恢复,负责人只关心备忘录的存储和管理,而备忘录本身则是一个被动的数据容器。这种职责分离使得代码结构清晰,易于维护。
