Skip to content

โช Memento Pattern: Pro-Pixel Undo History

๐Ÿ“ Overview

The Memento Pattern provides the ability to capture and externalize an object's internal state so that it can be restored later, without violating encapsulation. It is the architectural backbone of "Undo/Redo" functionality.

Core Concepts

  • Snapshot: Capturing the exact state of an object at a specific point in time.
  • Encapsulation: The object's internal details (private fields) are saved but remains hidden from the rest of the system.
  • History Management: A separate caretaker manages the timeline of snapshots.

๐Ÿญ The Engineering Story & Problem

๐Ÿ˜ก The Villain (The Problem)

You're building a professional text editor. You need an "Undo" feature. The "Snapshot Leaker" approach is to let the HistoryManager read the Editor's private variables (_text, _cursor, _scroll_y) and copy them into a list.
This creates Tight Coupling. The HistoryManager now depends on the internal implementation of the Editor. If you refactor the Editor to use a GapBuffer instead of a string, the HistoryManager breaks. You've leaked the internal state, violating encapsulation.

๐Ÿฆธ The Hero (The Solution)

The Memento Pattern solves this by creating a "Sealed Envelope" (the Memento).
The Editor (Originator) packs its own state into a Memento object. It hands this sealed envelope to the HistoryManager (Caretaker). The HistoryManager keeps it safe but cannot open it.
When the user hits Undo, the HistoryManager hands the envelope back to the Editor. The Editor opens it and restores its state. The HistoryManager never saw the data, preserving encapsulation.

๐Ÿ“œ Requirements & Constraints

  1. (Functional): implement a text editor with Undo functionality.
  2. (Technical): The History class must treat Mementos as opaque objects (no access to fields).
  3. (Technical): Mementos must be immutable deep copies of the state.

๐Ÿ—๏ธ Structure & Blueprint

Class Diagram

classDiagram
    direction TB
    class Editor {
        -content: string
        -cursor: int
        +type(text)
        +save() Memento
        +restore(memento)
    }
    class Memento {
        -content: string
        -cursor: int
        +get_state()
    }
    class History {
        -timeline: List~Memento~
        +push(memento)
        +undo()
    }

    Editor ..> Memento : Creates
    History o-- Memento : Manages

Runtime Context (Sequence)

sequenceDiagram
    participant User
    participant History
    participant Editor

    User->>Editor: type("Hello")
    User->>History: push(editor.save())
    History->>Editor: save()
    Editor-->>History: Memento A

    User->>Editor: type(" World")
    User->>History: undo()
    History->>Editor: restore(Memento A)
    Editor->>Editor: content = "Hello"

๐Ÿ’ป Implementation & Code

๐Ÿง  SOLID Principles Applied

  • Single Responsibility: Editor handles editing; History handles storage.
  • Open/Closed: The History mechanism works for any object that implements the Memento pattern.

๐Ÿ The Code

The Villain's Code (Without Pattern)
class History:
    def save(self, editor):
        # ๐Ÿ˜ก Direct access to private fields
        self.snapshots.append(editor._content)

    def undo(self, editor):
        # ๐Ÿ˜ก Direct modification of private fields
        editor._content = self.snapshots.pop()
The Hero's Code (With Pattern)
from types import MappingProxyType
from typing import Mapping, Optional
from copy import deepcopy

class Memento:
    def __init__(self, state: dict) -> None:
        self._state: MappingProxyType = MappingProxyType(state)

    @property
    def state(self) -> Mapping:
        return self._state.copy()


class History:
    def __init__(self) -> None:
        self.stack: list[Memento] = []
        self.redo_stack: list[Memento] = []

    def save(self, memento: Memento) -> None:
        self.stack.append(memento)
        self.redo_stack = []

    def undo(self, current_memento: Memento) -> Optional[Memento]:
        if not self.stack: return None
        self.redo_stack.append(current_memento)
        return self.stack.pop()

    def redo(self, current_memento: Memento) -> Optional[Memento]:
        if not self.redo_stack: return None
        self.stack.append(current_memento)
        return self.redo_stack.pop()


class Editor:
    def __init__(self, history: History) -> None:
        self.content: str = ""
        self.cursor: int = 0
        self.selection: tuple = (0, 0)
        self.history: History = history

    def type(self, text: str) -> None:
        self.content += text
        print(self.content)

    def create_snapshot(self) -> Memento:
        state = {
            "content": self.content,
            "cursor": self.cursor,
            "selection": self.selection
        }

        return Memento(state)

    def restore(self) -> None:
        state = self.history.undo(self.create_snapshot())
        if state:
            self.__dict__.update(state.state)
            print("Restoring")
            print(self.content)

    def unrestore(self) -> None:
        state = self.history.redo(self.create_snapshot())
        if state:
            self.__dict__.update(state.state)
            print("Redoing")
            print(self.content)

history = History()
editor = Editor(history)

editor.type("Hello")

history.save(editor.create_snapshot())

editor.type(" World")

history.save(editor.create_snapshot())

editor.type("!")

editor.restore()
editor.restore()

editor.unrestore()
editor.unrestore()

# class NHistory:
#     def __init__(self) -> None:
#         self.history: list[Memento] = []
#         self.future: list[Memento] = []

#     def save(self, snapshot: dict[str, Any]) -> None:
#         memento = Memento(snapshot)
#         self.history.append(memento)
#         self.future = []

#     def undo(self) -> Optional[dict[str, Any]]:
#         if self.history != []:
#             self.future.append(self.history[-1])
#             snapshot = self.history.pop().__dict__.copy()
#             snapshot["content"] = snapshot["_Memento__content"]
#             del snapshot["_Memento__content"]
#             snapshot["cursor"] = snapshot["_Memento__cursor"]
#             del snapshot["_Memento__cursor"]
#             snapshot["selection"] = snapshot["_Memento__selection"]
#             del snapshot["_Memento__selection"]
#             return snapshot

#     def redo(self) -> Optional[dict[str, Any]]:
#         if self.future != []:
#             snapshot = self.future.pop().__dict__.copy()
#             snapshot["content"] = snapshot["_Memento__content"]
#             del snapshot["_Memento__content"]
#             snapshot["cursor"] = snapshot["_Memento__cursor"]
#             del snapshot["_Memento__cursor"]
#             snapshot["selection"] = snapshot["_Memento__selection"]
#             del snapshot["_Memento__selection"]
#             return snapshot

# class NEditor:
#     def __init__(self, history: History):
#         self.content: str = ""
#         self.cursor: int = 0
#         self.selection: tuple = (0, 0)
#         self.history: History = history

#     def type(self, text: str) -> None:
#         self.content += text
#         self.history.future = []
#         print(self.content)

#     def create_snapshot(self) -> dict[str, Any]:
#         return self.__dict__.copy()

#     def restore(self, history: Optional[dict[str, Any]]) -> None:
#         if history:
#             self.__dict__.update(history)
#             print("Restoring")
#             print(self.content)

#     def unrestore(self, history: Optional[dict[str, Any]]) -> None:
#         if history:
#             self.__dict__.update(history)
#             print("Redo")
#             print(self.content)

# history = History()
# editor = Editor(history)

# editor.type("Hello")

# history.save(editor.create_snapshot())

# editor.type(" World")

# history.save(editor.create_snapshot())

# editor.type("!")

# editor.restore(history.undo())
# editor.restore(history.undo())

# editor.unrestore(history.redo())
# editor.unrestore(history.redo())

โš–๏ธ Trade-offs & Testing

Pros (Why it works) Cons (The Twist / Pitfalls)
Encapsulation: State remains private. RAM Usage: Saving full copies is memory intensive.
Separation: Originator and Caretaker are decoupled. Complexity: Needs extra Memento classes.
Safety: Caretaker cannot corrupt the state. Garbage Collection: Caretakers can keep obsolete states alive.

๐Ÿงช Testing Strategy

  1. Unit Test Editor: Verify type() changes state.
  2. Unit Test Undo: Type "A", Save, Type "B", Undo. Verify state is "A".
  3. Test Immutability: Verify that changing the Memento object externally (if possible) doesn't change the Editor.

๐ŸŽค Interview Toolkit

  • Interview Signal: Demonstrates understanding of encapsulation boundaries and restoration logic.
  • When to Use:
    • "Implement 'Ctrl+Z'..."
    • "Save game progress..."
    • "Transaction rollback..."
  • Scalability Probe: "How to implement Undo for a 1GB file?" (Answer: Don't save the whole file. Save the diff or operation (Command pattern) to reverse it.)
  • Design Alternatives:
    • Command Pattern: Often preferred for "lightweight" undo (reversing the operation) vs Memento's "heavyweight" undo (restoring the state).
  • Command โ€” Can implement undo by reversing operations.
  • Prototype โ€” Used to create the state copy for the Memento.