MidТеория4 min

Memento

Паттерн Memento в Go: сохранение и восстановление состояния, undo в редакторах и игровые сохранения

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)
Лучше для Простых объектов Сложных объектов с малыми изменениями

Проверь себя

Как Memento обеспечивает инкапсуляцию?

В чём главное отличие Memento от Command для реализации Undo?

Связанные темы