Memento (Хранитель)

Паттерн Memento (Снимок) позволяет сохранять и восстанавливать предыдущее состояние объекта, не раскрывая деталей его реализации. Он часто используется для реализации операций «Отмена» (Undo), восстановления состояния или отката к предыдущей версии данных.

Когда применять Memento

  • Когда нужно сохранять состояние объекта для последующего восстановления.
  • Когда прямое сохранение состояния нарушает инкапсуляцию.
  • Когда важно реализовать историю изменений (например, редакторы, игры, базы данных).

PlantUML-диаграмма

UML диаграмма Memento

// Снимок (Memento)
public class EditorMemento
{
    public string Text { get; }
    public int CursorPosition { get; }
    public DateTime Timestamp { get; }

    public EditorMemento(string text, int cursorPosition)
    {
        Text = text;
        CursorPosition = cursorPosition;
        Timestamp = DateTime.Now;
    }
}

// Создатель (Originator)
public class TextEditor
{
    private string _text = "";
    private int _cursor = 0;

    public void Type(string newText)
    {
        _text += newText;
        _cursor = _text.Length;
        Console.WriteLine($"Текущий текст: {_text}");
    }

    public EditorMemento Save()
    {
        Console.WriteLine("Состояние сохранено.");
        return new EditorMemento(_text, _cursor);
    }

    public void Restore(EditorMemento memento)
    {
        _text = memento.Text;
        _cursor = memento.CursorPosition;
        Console.WriteLine($"Состояние восстановлено: {_text}");
    }
}

// Хранитель (Caretaker)
public class History
{
    private Stack<EditorMemento> _history = new();

    public void Push(EditorMemento memento) => _history.Push(memento);
    public EditorMemento Pop() => _history.Pop();
}

// Клиентский код
class Program
{
    static void Main()
    {
        var editor = new TextEditor();
        var history = new History();

        editor.Type("Привет, ");
        history.Push(editor.Save());

        editor.Type("мир!");
        history.Push(editor.Save());

        editor.Type(" Как дела?");
        
        // Откат
        editor.Restore(history.Pop());
        editor.Restore(history.Pop());
    }
}
// Снимок: поля неэкспортируемые, снаружи состояние не подглядеть.
type EditorMemento struct {
	text      string
	cursor    int
	timestamp time.Time
}

type TextEditor struct {
	text   string
	cursor int
}

func (e *TextEditor) Type(newText string) {
	e.text += newText
	e.cursor = len([]rune(e.text))
	fmt.Printf("Текущий текст: %s\n", e.text)
}

func (e *TextEditor) Save() EditorMemento {
	fmt.Println("Состояние сохранено.")
	return EditorMemento{text: e.text, cursor: e.cursor, timestamp: time.Now()}
}

func (e *TextEditor) Restore(m EditorMemento) {
	e.text = m.text
	e.cursor = m.cursor
	fmt.Printf("Состояние восстановлено: %s\n", e.text)
}

type History struct {
	items []EditorMemento
}

func (h *History) Push(m EditorMemento) { h.items = append(h.items, m) }

func (h *History) Pop() EditorMemento {
	last := h.items[len(h.items)-1]
	h.items = h.items[:len(h.items)-1]
	return last
}

func main() {
	editor := &TextEditor{}
	history := &History{}

	editor.Type("Привет, ")
	history.Push(editor.Save())

	editor.Type("мир!")
	history.Push(editor.Save())

	editor.Type(" Как дела?")

	editor.Restore(history.Pop())
	editor.Restore(history.Pop())
}
from dataclasses import dataclass, field
from datetime import datetime


@dataclass(frozen=True)
class EditorMemento:
    text: str
    cursor_position: int
    timestamp: datetime = field(default_factory=datetime.now)


class TextEditor:
    def __init__(self) -> None:
        self._text = ""
        self._cursor = 0

    def type(self, new_text: str) -> None:
        self._text += new_text
        self._cursor = len(self._text)
        print(f"Текущий текст: {self._text}")

    def save(self) -> EditorMemento:
        print("Состояние сохранено.")
        return EditorMemento(self._text, self._cursor)

    def restore(self, memento: EditorMemento) -> None:
        self._text = memento.text
        self._cursor = memento.cursor_position
        print(f"Состояние восстановлено: {self._text}")


class History:
    def __init__(self) -> None:
        self._history: list[EditorMemento] = []

    def push(self, memento: EditorMemento) -> None:
        self._history.append(memento)

    def pop(self) -> EditorMemento:
        return self._history.pop()


editor = TextEditor()
history = History()

editor.type("Привет, ")
history.push(editor.save())

editor.type("мир!")
history.push(editor.save())

editor.type(" Как дела?")

editor.restore(history.pop())
editor.restore(history.pop())
class EditorMemento {
    readonly timestamp = new Date();

    constructor(
        readonly text: string,
        readonly cursorPosition: number,
    ) {}
}

class TextEditor {
    private text = "";
    private cursor = 0;

    type(newText: string): void {
        this.text += newText;
        this.cursor = this.text.length;
        console.log(`Текущий текст: ${this.text}`);
    }

    save(): EditorMemento {
        console.log("Состояние сохранено.");
        return new EditorMemento(this.text, this.cursor);
    }

    restore(memento: EditorMemento): void {
        this.text = memento.text;
        this.cursor = memento.cursorPosition;
        console.log(`Состояние восстановлено: ${this.text}`);
    }
}

class History {
    private readonly history: EditorMemento[] = [];

    push(memento: EditorMemento): void {
        this.history.push(memento);
    }

    pop(): EditorMemento | undefined {
        return this.history.pop();
    }
}

const editor = new TextEditor();
const history = new History();

editor.type("Привет, ");
history.push(editor.save());

editor.type("мир!");
history.push(editor.save());

editor.type(" Как дела?");

const first = history.pop();
if (first) editor.restore(first);

const second = history.pop();
if (second) editor.restore(second);
import java.time.LocalDateTime;
import java.util.ArrayDeque;
import java.util.Deque;

record EditorMemento(String text, int cursorPosition, LocalDateTime timestamp) {
    EditorMemento(String text, int cursorPosition) {
        this(text, cursorPosition, LocalDateTime.now());
    }
}

class TextEditor {
    private String text = "";
    private int cursor = 0;

    void type(String newText) {
        text += newText;
        cursor = text.length();
        System.out.println("Текущий текст: " + text);
    }

    EditorMemento save() {
        System.out.println("Состояние сохранено.");
        return new EditorMemento(text, cursor);
    }

    void restore(EditorMemento memento) {
        text = memento.text();
        cursor = memento.cursorPosition();
        System.out.println("Состояние восстановлено: " + text);
    }
}

class History {
    private final Deque<EditorMemento> history = new ArrayDeque<>();

    void push(EditorMemento memento) {
        history.push(memento);
    }

    EditorMemento pop() {
        return history.pop();
    }
}

public class Program {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();
        History history = new History();

        editor.type("Привет, ");
        history.push(editor.save());

        editor.type("мир!");
        history.push(editor.save());

        editor.type(" Как дела?");

        editor.restore(history.pop());
        editor.restore(history.pop());
    }
}
import java.time.LocalDateTime

data class EditorMemento(
    val text: String,
    val cursorPosition: Int,
    val timestamp: LocalDateTime = LocalDateTime.now(),
)

class TextEditor {
    private var text = ""
    private var cursor = 0

    fun type(newText: String) {
        text += newText
        cursor = text.length
        println("Текущий текст: $text")
    }

    fun save(): EditorMemento {
        println("Состояние сохранено.")
        return EditorMemento(text, cursor)
    }

    fun restore(memento: EditorMemento) {
        text = memento.text
        cursor = memento.cursorPosition
        println("Состояние восстановлено: $text")
    }
}

class History {
    private val history = ArrayDeque<EditorMemento>()

    fun push(memento: EditorMemento) = history.addLast(memento)

    fun pop(): EditorMemento? = history.removeLastOrNull()
}

fun main() {
    val editor = TextEditor()
    val history = History()

    editor.type("Привет, ")
    history.push(editor.save())

    editor.type("мир!")
    history.push(editor.save())

    editor.type(" Как дела?")

    history.pop()?.let { editor.restore(it) }
    history.pop()?.let { editor.restore(it) }
}

Плюсы и минусы

Плюсы Минусы
Позволяет сохранять и восстанавливать состояние без нарушения инкапсуляции Потребляет больше памяти при частом сохранении состояния
Упрощает реализацию «Отмены» и «Повтора» Сложность управления историей и временем хранения снимков
Хорошо работает с сериализацией и логированием Не подходит для очень больших объектов (много данных в снимке)

Заключение

Memento — поведенческий паттерн, который помогает реализовать систему восстановления состояния объектов. Он особенно полезен в текстовых редакторах, играх, undo/redo механизмах и других сценариях, где важно сохранить историю изменений.