Memento (Minne)
Introduktion
Memento är ett beteendemönster som låter dig spara och återställa ett objekts tidigare tillstånd utan att avslöja detaljerna i dess implementation.
Problem
Du bygger en textredigerare med ångra-funktion. För att spara tillstånd behöver du komma åt objektets interna fält — men om du exponerar dem bryter du inkapslingen och klientkoden blir beroende av implementationsdetaljer.
Lösning
Memento låter objektet självt skapa en ögonblicksbild (snapshot) av sitt tillstånd i ett Memento-objekt. Bara det ursprungliga objektet kan läsa data ur mementot — andra klasser ser bara ett ogenomskinligt skal.
När ska Memento användas?
- När du behöver ångra/gör om (undo/redo) utan att bryta inkapslingen.
- När direktåtkomst till ett objekts fält bryter mot inkapslingen.
- För att implementera transaktioner som kan rullas tillbaka.
Struktur
| Roll | Ansvar |
|---|---|
| Originator | Skapar mementos av sitt eget tillstånd och återställer från dem. |
| Memento | Lagrar tillståndet; exponerar ingenting till omvärlden. |
| Caretaker | Håller mementohistoriken men rör aldrig innehållet. |
Exempel — Avancerad textredigerare (Java)
Scenariot: En textredigerare som sparar hela sitt tillstånd (text, markörposition, urklipp) per ångrasteg.
import java.util.ArrayDeque;
import java.util.Deque;
// ── Memento (ogenomskinlig) ───────────────────────────────
public final class EditorMemento {
private final String text;
private final int cursorPosition;
private final String clipboard;
private final long timestamp;
// Bara Originator (Editor) skapar och läser Mementos
EditorMemento(String text, int cursorPosition, String clipboard) {
this.text = text;
this.cursorPosition = cursorPosition;
this.clipboard = clipboard;
this.timestamp = System.currentTimeMillis();
}
String getText() { return text; }
int getCursorPosition(){ return cursorPosition; }
String getClipboard() { return clipboard; }
@Override
public String toString() {
String preview = text.length() > 20 ? text.substring(0, 20) + "..." : text;
return "Snapshot['" + preview + "' pos=" + cursorPosition + "]";
}
}
// ── Originator ────────────────────────────────────────────
public class TextEditor {
private StringBuilder text = new StringBuilder();
private int cursorPosition = 0;
private String clipboard = "";
public void type(String input) {
text.insert(cursorPosition, input);
cursorPosition += input.length();
}
public void moveCursor(int position) {
this.cursorPosition = Math.max(0, Math.min(position, text.length()));
}
public void copy(int start, int end) {
clipboard = text.substring(start, Math.min(end, text.length()));
}
public void paste() {
text.insert(cursorPosition, clipboard);
cursorPosition += clipboard.length();
}
public void delete(int start, int end) {
text.delete(start, Math.min(end, text.length()));
cursorPosition = Math.min(cursorPosition, text.length());
}
// Spara tillstånd
public EditorMemento save() {
return new EditorMemento(text.toString(), cursorPosition, clipboard);
}
// Återställ tillstånd
public void restore(EditorMemento memento) {
this.text = new StringBuilder(memento.getText());
this.cursorPosition = memento.getCursorPosition();
this.clipboard = memento.getClipboard();
}
public String getText() { return text.toString(); }
public int getCursor() { return cursorPosition; }
@Override
public String toString() {
return "Text: '" + text + "' | Markör: " + cursorPosition + " | Urklipp: '" + clipboard + "'";
}
}
// ── Caretaker ─────────────────────────────────────────────
public class UndoManager {
private final Deque<EditorMemento> history = new ArrayDeque<>();
private final Deque<EditorMemento> redoStack = new ArrayDeque<>();
private final TextEditor editor;
public UndoManager(TextEditor editor) {
this.editor = editor;
}
public void save() {
EditorMemento snapshot = editor.save();
history.push(snapshot);
redoStack.clear(); // Ny åtgärd rensar redo-historiken
System.out.println("💾 Sparade: " + snapshot);
}
public void undo() {
if (history.isEmpty()) {
System.out.println("↩️ Inget att ångra.");
return;
}
redoStack.push(editor.save()); // Spara nuläget för redo
EditorMemento previous = history.pop();
editor.restore(previous);
System.out.println("↩️ Ångrade till: " + previous);
}
public void redo() {
if (redoStack.isEmpty()) {
System.out.println("↪️ Inget att göra om.");
return;
}
history.push(editor.save());
EditorMemento next = redoStack.pop();
editor.restore(next);
System.out.println("↪️ Gjorde om till: " + next);
}
}
// ── Klientkod ─────────────────────────────────────────────
public class Application {
public static void main(String[] args) {
TextEditor editor = new TextEditor();
UndoManager manager = new UndoManager(editor);
manager.save(); // Spara tomt initialtillstånd
editor.type("Hej världen!");
System.out.println("Efter type: " + editor);
manager.save();
editor.copy(4, 11); // Kopiera "världen"
editor.moveCursor(editor.getText().length());
editor.paste();
System.out.println("Efter paste: " + editor);
manager.save();
editor.delete(0, 4); // Ta bort "Hej "
System.out.println("Efter delete: " + editor);
manager.save();
System.out.println("\n--- Ångrar ---");
manager.undo();
System.out.println("Nu: " + editor);
manager.undo();
System.out.println("Nu: " + editor);
System.out.println("\n--- Gör om ---");
manager.redo();
System.out.println("Nu: " + editor);
}
}
Output:
Efter type: Text: 'Hej världen!' | Markör: 12 | Urklipp: ''
Efter paste: Text: 'Hej världen!världen' | Markör: 19 | Urklipp: 'världen'
Efter delete: Text: 'världen!världen' | Markör: 15 | Urklipp: 'världen'
--- Ångrar ---
↩️ Ångrade till: Snapshot['världen!världen' pos=15]
Nu: Text: 'Hej världen!världen' | Markör: 19 | Urklipp: 'världen'
↩️ Ångrade till: Snapshot['Hej världen!världen' pos=19]
Nu: Text: 'Hej världen!' | Markör: 12 | Urklipp: ''
--- Gör om ---
↪️ Gjorde om till: Snapshot['Hej världen!' pos=12]
Nu: Text: 'Hej världen!världen' | Markör: 19 | Urklipp: 'världen'
Fördelar
- Spara och återställ tillstånd utan att bryta inkapslingen.
- Caretaker behöver inte känna till Originatorns interna struktur.
- Enkelt att implementera undo/redo och transaktioner.
Nackdelar
- Kan förbruka mycket minne om tillstånden är stora eller sparas ofta.
- Caretaker måste hålla reda på Originatorns livscykel för att kunna rensa gamla mementos.
Av Victor Hernandez från Bytebase.se