Memento

Wzorce behawioralne

w trakcie

1. Definicja

Memento (ang. pamiątka) to wzorzec projektowy, który umożliwia zapisanie i przywrócenie poprzedniego stanu obiektu bez ujawniania szczegółów jego implementacji.

Without violating encapsulation, capture and externalize an object's internal state so that the object can be restored to this state later. — Gang of Four, Design Patterns

Najprościej rzecz ujmując: Memento to możliwość cofnięcia zmian — jak Ctrl+Z w edytorze tekstu.

Wzorzec odpowiada na pytanie: jak zapisać stan obiektu w danym momencie, żeby móc do niego wrócić później — a jednocześnie nie naruszać zasady enkapsulacji i nie eksponować wewnętrznych pól klasy na zewnątrz?


2. Przykład z życia: zobaczmy jak to zrozumieć?

Wyobraźmy sobie, że gramy w szachy na turnieju. W pewnym momencie chcemy przetestować ryzykowne posunięcie — ale nie jesteśmy pewni, czy to dobry ruch. Co byśmy zrobili?

Zapisalibyśmy aktualną pozycję figur na kartce — rodzaj fotografii stanu planszy — a potem wykonali ruch. Jeśli okaże się błędem, po prostu wrócimy do zapisanej pozycji i rozstawimy figury od nowa.

Zwróćmy uwagę na coś ważnego: kartka z pozycją figur nie jest samą planszą. To oddzielny obiekt, który przechowuje informację o stanie. Plansza nie musi wiedzieć, że kartka istnieje. Gracz, który trzyma kartkę, nie musi rozumieć reguł gry — on po prostu przechowuje zapis i w razie potrzeby go zwraca.

To właśnie esencja wzorca Memento:

  • Originator (plansza szachowa) — obiekt, którego stan chcemy zapisać,
  • Memento (kartka z pozycją figur) — nienaruszalny zapis stanu,
  • Caretaker (gracz trzymający kartkę) — przechowuje Memento, ale nie zagląda do jego wnętrza.

Ćwiczenie myślowe: wyobraźmy sobie edytor tekstu. Piszemy dokument, usuwamy akapit, a potem chcemy go odzyskać. Ctrl+Z przywraca poprzedni stan — ale jak to działa pod spodem? Editor musi gdzieś trzymać historię stanów. Każde wciśnięcie Ctrl+Z to sięgnięcie po ostatni zapis ze stosu i przywrócenie treści dokumentu do tamtego momentu. Dokładnie tak działa Memento — zobaczmy to w kodzie.


3. Przykład — naruszenie Memento (bez wzorca)

Wyobraźmy sobie, że tworzymy prosty edytor tekstu. Użytkownik może pisać, a my chcemy dać mu możliwość cofnięcia ostatniej zmiany.

Naiwne podejście: niech edytor sam w sobie przechowuje historię stanów jako listę Stringów. Caretaker (np. klasa Application) bezpośrednio odczytuje i manipuluje wewnętrzną strukturą edytora.

class TextEditor {
    // Wewnętrzny stan — bezpośrednio dostępny z zewnątrz
    public String content;
    // Historia jako publiczna lista — naruszenie enkapsulacji
    public List<String> history = new ArrayList<>();

    public void type(String text) {
        // Zapisujemy poprzedni stan przed zmianą
        history.add(content);
        content = content + text;
    }
}

class Application {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();
        editor.content = "";

        editor.type("Cześć, ");
        editor.type("świecie!");

        System.out.println(editor.content); // "Cześć, świecie!"

        // Caretaker bezpośrednio grzebie w wewnętrznej historii edytora
        // To naruszenie enkapsulacji — zewnętrzny kod zna strukturę wnętrza
        if (!editor.history.isEmpty()) {
            editor.content = editor.history.remove(editor.history.size() - 1);
        }

        System.out.println(editor.content); // "Cześć, "
    }
}

Co jest złe w tym podejściu?

Klasa Application zna szczegóły implementacyjne TextEditor — wie, że historia to List<String>, i wie, jak z niej korzystać. To naruszenie enkapsulacji.

Konkretne konsekwencje:

  • Jeśli zdecydujemy się zmienić strukturę historii (np. z List na Deque), musimy też zmienić kod w Application — i w każdym innym miejscu, które korzysta z historii,
  • Zewnętrzny kod może przypadkowo zmodyfikować historię edytora — np. wyczyścić ją, wstawić nieprawidłowy stan, zmienić kolejność,
  • Testowanie jest trudne: żeby przetestować funkcję "cofnij", musimy znać wewnętrzną strukturę edytora i ręcznie nią manipulować,
  • Klasa TextEditor ma dwa powody do zmiany: logika edycji i zarządzanie historią (naruszenie SRP).

4. Przykład — zgodny z Memento

Jak podejdziemy do tego lepiej? Wydzielamy trzy osobne role: edytor wie, jak zapisać swój stan w obiekcie Memento i jak z niego przywrócić — ale to Caretaker decyduje, kiedy zapisać i kiedy przywrócić, a sam obiekt Memento jest nieprzezroczysty dla świata zewnętrznego.

// Memento — "kartka z pozycją figur"
// Przechowuje stan edytora; dostęp tylko przez Originator
class TextEditorMemento {
    // Pole prywatne — Caretaker nie może zajrzeć do środka
    private final String savedContent;

    // Konstruktor dostępny tylko przez Originator (w Javie można użyć package-private)
    TextEditorMemento(String content) {
        this.savedContent = content;
    }

    // Metoda dostępna tylko przez Originator — Caretaker jej nie używa
    String getSavedContent() {
        return savedContent;
    }
}

// Originator — "plansza szachowa"
// Jedyna klasa, która wie, co i jak zapisać
class TextEditor {
    private String content = "";

    public void type(String text) {
        content = content + text;
    }

    public String getContent() {
        return content;
    }

    // Originator tworzy Memento ze swojego aktualnego stanu
    public TextEditorMemento save() {
        return new TextEditorMemento(content);
    }

    // Originator przywraca stan z Memento — bez udziału Caretakera
    public void restore(TextEditorMemento memento) {
        this.content = memento.getSavedContent();
    }
}

// Caretaker — "gracz trzymający kartkę"
// Zarządza historią Memento, ale nigdy nie zagląda do ich wnętrza
class EditorHistory {
    private final Deque<TextEditorMemento> history = new ArrayDeque<>();

    public void push(TextEditorMemento memento) {
        history.push(memento);
    }

    public TextEditorMemento pop() {
        return history.pop();
    }

    public boolean isEmpty() {
        return history.isEmpty();
    }
}

// Użycie
class Application {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();
        EditorHistory history = new EditorHistory();

        // Piszemy i zapisujemy stan przed każdą zmianą
        editor.type("Cześć, ");
        history.push(editor.save()); // zapisujemy stan "Cześć, "

        editor.type("świecie!");
        System.out.println(editor.getContent()); // "Cześć, świecie!"

        // Cofamy ostatnią zmianę — Ctrl+Z
        if (!history.isEmpty()) {
            editor.restore(history.pop());
        }

        System.out.println(editor.getContent()); // "Cześć, "
    }
}

Co zyskujemy krok po kroku?

Klasa TextEditorMemento ma prywatne pole savedContentEditorHistory (Caretaker) trzyma obiekty Memento na stosie, ale nigdy nie czyta ich zawartości. Wie tylko, że coś trzyma.

Klasa TextEditor (Originator) jest jedyną klasą, która wie, co zapisać i jak z tego skorzystać. Metoda save() tworzy Memento, a restore() z niego korzysta — bez żadnego udziału zewnętrznego kodu.

Klasa EditorHistory (Caretaker) może spokojnie zmienić Deque na List, dodać limit historii, dodać wiele poziomów cofania — i żadna z pozostałych klas nie musi wiedzieć o tej zmianie.

Porównanie przed/po:

BEZ MEMENTO                        Z MEMENTO
──────────────────────────────     ──────────────────────────────
Application zna List<String>       Application zna tylko EditorHistory
Application manipuluje historią    EditorHistory jest nieprzezroczysty
TextEditor eksponuje wnętrze       TextEditor chroni swój stan
Zmiana struktury → zmiany wszędzie Zmiana struktury → lokalnie w Caretaker

5. Diagram

┌─────────────────────────────────────────────────────────────┐
│                         MEMENTO                             │
└─────────────────────────────────────────────────────────────┘

  ┌──────────────┐    save()     ┌──────────────────┐
  │  Originator  │ ────────────► │     Memento      │
  │ (TextEditor) │               │ (snapshot stanu) │
  │              │ ◄──────────── │                  │
  └──────────────┘   restore()   └──────────────────┘
         ▲                               │
         │                               │ przechowuje
         │                               ▼
         │                      ┌──────────────────┐
         └──────────────────────│    Caretaker     │
              restore(pop())    │ (EditorHistory)  │
                                │  [stack/deque]   │
                                └──────────────────┘

  Caretaker NIE zagląda do wnętrza Memento.
  Tylko Originator czyta i zapisuje swój stan.

6. Dlaczego Memento jest ważne?

Enkapsulacja pozostaje nienaruszona. Zewnętrzne klasy nie muszą wiedzieć, jak wygląda wewnętrzny stan Originatora. Dzięki temu możemy dowolnie zmieniać implementację — np. dodać nowe pola do TextEditor — i wystarczy zaktualizować metody save() i restore(). Caretaker nie wymaga żadnych zmian.

Łatwe testowanie. Chcąc przetestować funkcję cofania, tworzymy edytor, wywołujemy save(), modyfikujemy stan, wywołujemy restore() i sprawdzamy wynik. Nie musimy znać wewnętrznej struktury klasy.

Kontrola nad historią. Caretaker może implementować różne strategie: ograniczoną liczbę kroków cofania, redo (cofanie cofnięcia), punkty zapisu w grach, migawki konfiguracji. Cała ta logika żyje w Caretakerze i nie zaśmieca Originatora.

Wpływ na pracę zespołu. Gdy jeden programista pracuje nad logiką edytora, a drugi nad zarządzaniem historią — nie wchodzą sobie w drogę. Kontrakty między klasami są jasno zdefiniowane przez interfejs save()/restore().

Konsekwencje zaniedbania w dużych projektach są dotkliwe: logika "cofnij/ponów" rozlewa się po całej aplikacji, stan obiektu jest eksponowany na zewnątrz i podatny na przypadkowe modyfikacje, a dodanie nowego pola do Originatora wymaga aktualizacji kodu w dziesiątkach miejsc.


7. Kiedy stosować?

Edytory i narzędzia twórcze. Każdy edytor tekstu, graficzny, kodu — wszędzie tam, gdzie użytkownik oczekuje Ctrl+Z. Wzorzec jest tu naturalnym rozwiązaniem.

Gry. Punkty zapisu (save points) to klasyczne Memento: gra zapisuje pełny stan świata gry (pozycje wrogów, ekwipunek, HP) do obiektu, który można później wczytać.

Transakcje i operacje odwracalne. Jeśli operacja może się nie powieść, zapisujemy stan przed jej wykonaniem. W razie błędu przywracamy poprzedni stan — prymitywna forma transakcji bez bazy danych.

Konfiguracja z możliwością restartu. Aplikacje, które pozwalają "przywrócić ustawienia domyślne" lub cofnąć ostatnią zmianę konfiguracji.

Kiedy Memento jest szczególnie cenne: gdy Originator ma złożony stan zbudowany z wielu pól, gdy nie możemy lub nie chcemy eksponować jego wewnętrznej struktury, gdy potrzebujemy wielu niezależnych punktów przywracania.

Kiedy uważać: Memento może być kosztowne pamięciowo — każdy zapis to kopia stanu. Przy dużych obiektach lub częstych zapisach warto rozważyć zapis przyrostowy (tylko różnic) albo ograniczenie głębokości historii.

W ekosystemie Java i frameworkach:

  • java.io.Serializable — serializacja obiektu do strumienia to konceptualnie zapis Memento na dysk,
  • mechanizm undo/redo w javax.swing.undo.UndoManager implementuje wariant tego wzorca,
  • snapshoty w systemach baz danych (np. Redis BGSAVE) to Memento na poziomie infrastruktury.

"Zrób zdjęcie, zanim cokolwiek zmienisz." Memento to aparat fotograficzny dla stanu obiektu — Originator robi zdjęcie, Caretaker trzyma album, a zdjęcia nie można podrobić.