Memento (Снимок)
Проблема
Нужно сохранять состояние объекта для последующего восстановления, не нарушая инкапсуляцию. Клиент не должен знать деталей внутреннего состояния -- он просто хранит "снимки".
Диаграмма
Originator Memento Caretaker
(Editor) (Snapshot) (History)
+----------+ +----------+ +----------+
| state |---->| state | | mementos |
+----------+ +----------+ | [0] [1] |
| Save() | | (opaque) | | [2] ... |
| Restore()| +----------+ +----------+
+----------+
Editor.Save() --> Snapshot --> History stores it
History.Undo() --> Snapshot --> Editor.Restore()
Реализация: Текстовый редактор
package editor
import (
"fmt"
"time"
)
// Snapshot is the memento -- an opaque state holder.
// Fields are unexported to preserve encapsulation.
type Snapshot struct {
content string
cursorPos int
selection [2]int // start, end
savedAt time.Time
scrollY int
}
// SavedAt returns when the snapshot was created.
func (s *Snapshot) SavedAt() time.Time {
return s.savedAt
}
// Editor is the originator -- it creates and restores snapshots.
type Editor struct {
content string
cursorPos int
selection [2]int
scrollY int
}
// NewEditor creates a new text editor.
func NewEditor() *Editor {
return &Editor{}
}
// Type adds text at cursor position.
func (e *Editor) Type(text string) {
before := e.content[:e.cursorPos]
after := e.content[e.cursorPos:]
e.content = before + text + after
e.cursorPos += len(text)
}
// MoveCursor sets the cursor position.
func (e *Editor) MoveCursor(pos int) {
if pos < 0 {
pos = 0
}
if pos > len(e.content) {
pos = len(e.content)
}
e.cursorPos = pos
}
// Select sets the selection range.
func (e *Editor) Select(start, end int) {
e.selection = [2]int{start, end}
}
// Content returns the current text.
func (e *Editor) Content() string {
return e.content
}
// Save creates a memento of the current state.
func (e *Editor) Save() *Snapshot {
return &Snapshot{
content: e.content,
cursorPos: e.cursorPos,
selection: e.selection,
scrollY: e.scrollY,
savedAt: time.Now(),
}
}
// Restore recovers the state from a memento.
func (e *Editor) Restore(s *Snapshot) {
e.content = s.content
e.cursorPos = s.cursorPos
e.selection = s.selection
e.scrollY = s.scrollY
}
// String returns a debug representation.
func (e *Editor) String() string {
return fmt.Sprintf("content=%q cursor=%d", e.content, e.cursorPos)
}
Caretaker (History)
// History is the caretaker -- it stores mementos.
type History struct {
snapshots []*Snapshot
maxSize int
}
// NewHistory creates a history with the given capacity.
func NewHistory(maxSize int) *History {
return &History{
snapshots: make([]*Snapshot, 0, maxSize),
maxSize: maxSize,
}
}
// Push saves a snapshot.
func (h *History) Push(s *Snapshot) {
if len(h.snapshots) >= h.maxSize {
// Remove oldest snapshot
h.snapshots = h.snapshots[1:]
}
h.snapshots = append(h.snapshots, s)
}
// Pop retrieves and removes the last snapshot.
func (h *History) Pop() (*Snapshot, bool) {
if len(h.snapshots) == 0 {
return nil, false
}
last := h.snapshots[len(h.snapshots)-1]
h.snapshots = h.snapshots[:len(h.snapshots)-1]
return last, true
}
// Len returns the number of stored snapshots.
func (h *History) Len() int {
return len(h.snapshots)
}
Использование
func main() {
editor := editor.NewEditor()
history := editor.NewHistory(50)
// Type some text, saving snapshots
history.Push(editor.Save())
editor.Type("Hello")
fmt.Println(editor) // content="Hello" cursor=5
history.Push(editor.Save())
editor.Type(", World!")
fmt.Println(editor) // content="Hello, World!" cursor=13
history.Push(editor.Save())
editor.MoveCursor(5)
editor.Type(" Beautiful")
fmt.Println(editor) // content="Hello Beautiful, World!" cursor=15
// Undo: restore previous snapshot
if snapshot, ok := history.Pop(); ok {
editor.Restore(snapshot)
}
fmt.Println(editor) // content="Hello, World!" cursor=13
if snapshot, ok := history.Pop(); ok {
editor.Restore(snapshot)
}
fmt.Println(editor) // content="Hello" cursor=5
}
Реальный пример: Game Save
package game
import (
"encoding/json"
"fmt"
"os"
"time"
)
// GameState holds all game data.
type GameState struct {
PlayerName string
Level int
HP int
MaxHP int
XP int
Gold int
Inventory []string
Position [2]float64
}
// SaveData is the memento for game state.
type SaveData struct {
State GameState
SavedAt time.Time
Name string
}
// Game is the originator.
type Game struct {
state GameState
}
// NewGame creates a new game.
func NewGame(playerName string) *Game {
return &Game{
state: GameState{
PlayerName: playerName,
Level: 1,
HP: 100,
MaxHP: 100,
XP: 0,
Gold: 50,
Inventory: []string{"wooden sword", "health potion"},
Position: [2]float64{0, 0},
},
}
}
// Save creates a game save.
func (g *Game) Save(name string) *SaveData {
// Deep copy inventory
inv := make([]string, len(g.state.Inventory))
copy(inv, g.state.Inventory)
return &SaveData{
State: GameState{
PlayerName: g.state.PlayerName,
Level: g.state.Level,
HP: g.state.HP,
MaxHP: g.state.MaxHP,
XP: g.state.XP,
Gold: g.state.Gold,
Inventory: inv,
Position: g.state.Position,
},
SavedAt: time.Now(),
Name: name,
}
}
// Load restores game state from a save.
func (g *Game) Load(save *SaveData) {
inv := make([]string, len(save.State.Inventory))
copy(inv, save.State.Inventory)
g.state = save.State
g.state.Inventory = inv
}
// SaveToFile persists a save to disk.
func SaveToFile(save *SaveData, path string) error {
data, err := json.MarshalIndent(save, "", " ")
if err != nil {
return fmt.Errorf("marshal save: %w", err)
}
return os.WriteFile(path, data, 0644)
}
// LoadFromFile reads a save from disk.
func LoadFromFile(path string) (*SaveData, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read save: %w", err)
}
var save SaveData
if err := json.Unmarshal(data, &save); err != nil {
return nil, fmt.Errorf("unmarshal save: %w", err)
}
return &save, nil
}
Когда использовать
Используйте, когда:
- Undo/Redo функциональность (совместно с Command)
- Игровые сохранения
- Checkpoint-recovery в долгих операциях
- Транзакционное поведение (сохранить -> попробовать -> откатить)
Не используйте, когда:
- Состояние объекта слишком большое для частого копирования
- Нет потребности в восстановлении
- Достаточно Command для undo
Memento vs Command для Undo
| Критерий | Memento | Command |
|---|---|---|
| Что хранит | Полный снимок состояния | Действие + данные для отмены |
| Память | Больше (полная копия) | Меньше (только дельта) |
| Реализация | Проще (Save/Restore) | Сложнее (Execute/Undo) |
| Лучше для | Простых объектов | Сложных объектов с малыми изменениями |