Memento pattern w praktyce

TL;DR: Memento pattern pozwala zapisywać i przywracać poprzednie stany obiektów bez naruszania enkapsulacji. Idealny do funkcji undo/redo, snapshots i przywracania konfiguracji. W Javie implementujesz go przez klasy Memento, Originator i Caretaker.

Dlaczego Memento pattern jest ważny

W każdej aplikacji użytkownicy popełniają błędy i chcą je cofnąć. Memento pattern to fundament dla funkcji „Cofnij” w edytorach tekstu, grach czy aplikacjach graficznych. Bez tego wzorca musielibyśmy naruszać enkapsulację obiektów lub tworzyć skomplikowane systemy kopiowania stanu.

Memento pattern należy do grupy wzorców behawioralnych i jest kluczowy w aplikacjach wymagających cofania operacji lub tworzenia punktów kontrolnych.

Co się nauczysz

  • Jak działa wzorzec Memento i kiedy go używać
  • Implementacja wszystkich trzech klas: Memento, Originator, Caretaker
  • Tworzenie systemu undo/redo w praktyce
  • Optymalizacja pamięci przy przechowywaniu stanów
  • Integracja z GUI i obsługa błędów

Wymagania wstępne

Powinieneś znać: podstawy programowania obiektowego w Javie, enkapsulację, konstruktory i metody. Przydatna znajomość kolekcji Java (ArrayList, Stack).

Czym jest wzorzec Memento

Memento pattern to wzorzec projektowy który pozwala zapisać wewnętrzny stan obiektu i przywrócić go później, nie naruszając zasad enkapsulacji. Składa się z trzech głównych komponentów:

Originator – obiekt którego stan chcemy zapisać
Memento – przechowuje snapshot stanu Originator

Caretaker – zarządza kolekcją Memento, ale nie ma dostępu do ich zawartości
Wyobraź sobie grę wideo z funkcją zapisywania. Originator to postać gracza, Memento to plik zapisu gry, a Caretaker to menu ładowania zapisów – może nimi zarządzać ale nie modyfikować bezpośrednio.

Podstawowa implementacja w Javie

Zacznijmy od prostego przykładu – edytor tekstu z funkcją cofania:

// Klasa Memento - przechowuje stan
class TextMemento {
    private final String state;
    private final long timestamp;
    
    public TextMemento(String state) {
        this.state = state;
        this.timestamp = System.currentTimeMillis();
    }
    
    public String getState() {
        return state;
    }
    
    public long getTimestamp() {
        return timestamp;
    }
}

// Klasa Originator - obiekt którego stan zapisujemy  
class TextEditor {
    private String content;
    
    public void write(String text) {
        this.content = text;
    }
    
    public String getContent() {
        return content;
    }
    
    // Tworzy memento z aktualnym stanem
    public TextMemento save() {
        return new TextMemento(content);
    }
    
    // Przywraca stan z memento
    public void restore(TextMemento memento) {
        this.content = memento.getState();
    }
}

Caretaker – zarządca stanów

Caretaker odpowiada za przechowywanie i zarządzanie kolekcją memento:

import java.util.ArrayList;
import java.util.List;

class EditorHistory {
    private List history = new ArrayList<>();
    private int currentPosition = -1;
    
    // Zapisuje nowy stan
    public void save(TextEditor editor) {
        // Usuwa przyszłe stany jeśli cofnęliśmy się wcześniej
        if (currentPosition < history.size() - 1) {
            history = history.subList(0, currentPosition + 1);
        }
        
        history.add(editor.save());
        currentPosition++;
    }
    
    // Cofnij ostatnią operację
    public boolean undo(TextEditor editor) {
        if (currentPosition > 0) {
            currentPosition--;
            editor.restore(history.get(currentPosition));
            return true;
        }
        return false;
    }
    
    // Ponów cofniętą operację  
    public boolean redo(TextEditor editor) {
        if (currentPosition < history.size() - 1) {
            currentPosition++;
            editor.restore(history.get(currentPosition));
            return true;
        }
        return false;
    }
    
    public boolean canUndo() {
        return currentPosition > 0;
    }
    
    public boolean canRedo() {
        return currentPosition < history.size() - 1;
    }
}
Pro tip: Zawsze sprawdzaj czy undo/redo jest możliwe przed wykonaniem operacji. Zapobiega to RuntimeException i poprawia UX.

Praktyczny przykład użycia

Zobaczmy jak połączyć wszystkie elementy w działającą aplikację:

public class MementoDemo {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();
        EditorHistory history = new EditorHistory();
        
        // Pierwsza wersja tekstu
        editor.write("Hello World");
        history.save(editor);
        System.out.println("Current: " + editor.getContent());
        
        // Modyfikacja tekstu
        editor.write("Hello World from Java");
        history.save(editor);
        System.out.println("Current: " + editor.getContent());
        
        // Kolejna modyfikacja
        editor.write("Hello World from Java Memento Pattern");
        history.save(editor);
        System.out.println("Current: " + editor.getContent());
        
        // Cofamy dwie operacje
        if (history.canUndo()) {
            history.undo(editor);
            System.out.println("After undo: " + editor.getContent());
        }
        
        if (history.canUndo()) {
            history.undo(editor);  
            System.out.println("After second undo: " + editor.getContent());
        }
        
        // Przywracamy jedną operację
        if (history.canRedo()) {
            history.redo(editor);
            System.out.println("After redo: " + editor.getContent());
        }
    }
}

Uwaga: Przechowywanie każdego stanu może szybko zużyć pamięć. W aplikacjach produkcyjnych ogranicz liczbę przechowywanych stanów (np. maksymalnie 50 operacji undo).

Optymalizacja i najlepsze praktyki

Ograniczanie zużycia pamięci

class OptimizedEditorHistory {
    private static final int MAX_HISTORY_SIZE = 50;
    private List history = new ArrayList<>();
    private int currentPosition = -1;
    
    public void save(TextEditor editor) {
        // Usuwa najstarsze stany jeśli przekroczono limit
        if (history.size() >= MAX_HISTORY_SIZE) {
            history.remove(0);
            if (currentPosition > 0) {
                currentPosition--;
            }
        }
        
        // Standardowa logika zapisywania
        if (currentPosition < history.size() - 1) {
            history = history.subList(0, currentPosition + 1);
        }
        
        history.add(editor.save());
        currentPosition++;
    }
    
    // Metoda czyszcząca historię
    public void clearHistory() {
        history.clear();
        currentPosition = -1;
    }
}

Memento z wieloma właściwościami

W rzeczywistych aplikacjach często zapisujemy złożone stany:

// Bardziej złożony Memento
class DocumentMemento {
    private final String content;
    private final String title;
    private final boolean saved;
    private final int cursorPosition;
    
    public DocumentMemento(String content, String title, 
                          boolean saved, int cursorPosition) {
        this.content = content;
        this.title = title;
        this.saved = saved;
        this.cursorPosition = cursorPosition;
    }
    
    // Gettery dla wszystkich pól
    public String getContent() { return content; }
    public String getTitle() { return title; }
    public boolean isSaved() { return saved; }
    public int getCursorPosition() { return cursorPosition; }
}

class Document {
    private String content;
    private String title;
    private boolean saved;
    private int cursorPosition;
    
    // Konstruktory, settery, gettery...
    
    public DocumentMemento save() {
        return new DocumentMemento(content, title, saved, cursorPosition);
    }
    
    public void restore(DocumentMemento memento) {
        this.content = memento.getContent();
        this.title = memento.getTitle();
        this.saved = memento.isSaved();
        this.cursorPosition = memento.getCursorPosition();
    }
}

Typowy błąd: Przechowywanie referencji zamiast kopii wartości w Memento. Zawsze kopiuj wartości, nie referencje, aby uniknąć przypadkowych modyfikacji.

Kiedy używać wzorca Memento

Idealny do:

  • Funkcji undo/redo w edytorach i aplikacjach graficznych
  • Snapshots konfiguracji aplikacji
  • Checkpointów w grach
  • Rollback operacji w systemach transakcyjnych
  • Przywracania stanu po błędach

Alternatywy i porównania

WzorzecZastosowanieZaletyWady
MementoUndo/RedoEnkapsulacja zachowanaZużycie pamięci
CommandUndo operacjiElastycznośćZłożoność implementacji
PrototypeKlonowanieSzybkośćShallow vs deep copy
Czy Memento pattern jest wydajny pamięciowo?

Może zużywać dużo pamięci jeśli przechowujesz wiele stanów złożonych obiektów. Zawsze ogranicz liczbę przechowywanych memento i rozważ kompresję dla dużych stanów.

Jak różni się od wzorca Command?

Command zapisuje operacje do wykonania, Memento zapisuje stan obiektu. Command może być bardziej elastyczny dla undo, ale Memento jest prostszy gdy potrzebujesz przywrócić dokładny stan obiektu.

Czy można łączyć Memento z innymi wzorcami?

Tak! Często łączy się z Command (undo), Observer (powiadamianie o zmianach stanu) lub Strategy (różne sposoby zapisywania stanu).

Jak testować implementację Memento?

Testuj czy stan zostaje przywrócony dokładnie, czy undo/redo działają poprawnie, czy ograniczenia pamięci są respektowane i czy enkapsulacja jest zachowana.

Co z serializacją Memento?

Jeśli chcesz zapisywać stany na dysk, implementuj Serializable w klasie Memento. Pamiętaj o versioning jeśli struktura obiektu może się zmieniać.

Czy Memento narusza enkapsulację?

Nie, jeśli implementujesz go poprawnie. Klasa Memento powinna być dostępna tylko dla Originator (można użyć nested class) i Caretaker nie powinien mieć dostępu do wewnętrznej struktury Memento.

Jak obsłużyć błędy przy przywracaniu stanu?

Zawsze waliduj Memento przed przywróceniem, obsługuj wyjątki gracefully i rozważ rollback do poprzedniego prawidłowego stanu jeśli przywracanie się nie powiedzie.

Przydatne zasoby

🚀 Zadanie dla Ciebie

Stwórz aplikację "Kalkulator z historią" która:

  • Wykonuje proste operacje matematyczne (+, -, *, /)
  • Zapisuje każdy wynik jako Memento
  • Pozwala cofnąć ostatnie N operacji
  • Ma ograniczenie do 20 zapisanych stanów
  • Wyświetla historię operacji z możliwością przeskoku do dowolnego punktu

Bonusowe punkty za dodanie funkcji zapisywania/wczytywania historii do pliku!

Wzorzec Memento to potężne narzędzie gdy potrzebujesz zarządzać stanem obiektów bez naruszania enkapsulacji. Pamiętaj o optymalizacji pamięci i zawsze testuj edge cases przy undo/redo. A jakie aplikacje z funkcją "Cofnij" używasz najczęściej?

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry