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.
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
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:
Memento – przechowuje snapshot stanu Originator
Caretaker – zarządza kolekcją Memento, ale nie ma dostępu do ich zawartości
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 Listhistory = 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; } }
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()); } } }
Optymalizacja i najlepsze praktyki
Ograniczanie zużycia pamięci
class OptimizedEditorHistory { private static final int MAX_HISTORY_SIZE = 50; private Listhistory = 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(); } }
Kiedy używać wzorca Memento
- 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
Wzorzec | Zastosowanie | Zalety | Wady |
---|---|---|---|
Memento | Undo/Redo | Enkapsulacja zachowana | Zużycie pamięci |
Command | Undo operacji | Elastyczność | Złożoność implementacji |
Prototype | Klonowanie | Szybkość | Shallow vs deep copy |
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.
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.
Tak! Często łączy się z Command (undo), Observer (powiadamianie o zmianach stanu) lub Strategy (różne sposoby zapisywania stanu).
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.
Jeśli chcesz zapisywać stany na dysk, implementuj Serializable w klasie Memento. Pamiętaj o versioning jeśli struktura obiektu może się zmieniać.
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.
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?