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

RollAnsvar
OriginatorSkapar mementos av sitt eget tillstånd och återställer från dem.
MementoLagrar tillståndet; exponerar ingenting till omvärlden.
CaretakerHå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