ExerciseDifficulty: Advanced2 min

Practice: Naming the Participants in a Form With Undo

A form, a history list and the snapshot they pass between them.

A form with an undo button uses the Memento pattern, and this exercise asks you to name each participant and say who may read what. Read the listing, then answer the questions before opening the solution.

py
from dataclasses import dataclass


@dataclass(frozen=True)
class FormSnapshot:
    _values: tuple[tuple[str, str], ...]


class SignupForm:
    def __init__(self) -> None:
        self._values: dict[str, str] = {"name": "", "email": "", "plan": "free"}

    def fill(self, field: str, value: str) -> None:
        self._values[field] = value

    def capture(self) -> FormSnapshot:
        return FormSnapshot(tuple(self._values.items()))

    def reset_to(self, snapshot: FormSnapshot) -> None:
        self._values = dict(snapshot._values)

    def show(self) -> None:
        print("  form:", self._values)


class UndoStack:
    def __init__(self) -> None:
        self._items: list[FormSnapshot] = []

    def push(self, snapshot: FormSnapshot) -> None:
        self._items.append(snapshot)

    def pop(self) -> FormSnapshot | None:
        return self._items.pop() if self._items else None


form = SignupForm()
undo = UndoStack()

undo.push(form.capture())
form.fill("name", "Kim")
undo.push(form.capture())
form.fill("plan", "pro")
form.show()

snapshot = undo.pop()
if snapshot is not None:
    form.reset_to(snapshot)
form.show()
A signup form, its snapshots, and an undo stack.
  1. Which class is the originator, which the memento, and which the caretaker?
  2. Which classes read the saved field values, and which only pass a snapshot along?
  3. What does the form show after the undo?
  4. Python does not enforce the underscore in _values. What would break the pattern if UndoStack ignored it?
Show a solution
Each class in the form and its part in the pattern
ClassParticipantReads the saved values
SignupFormOriginatorYes, in reset_to
FormSnapshotMementoHolds them, as _values
UndoStackCaretakerNo; it only pushes and pops snapshots
py
from dataclasses import dataclass


@dataclass(frozen=True)
class FormSnapshot:
    _values: tuple[tuple[str, str], ...]


class SignupForm:
    def __init__(self) -> None:
        self._values: dict[str, str] = {"name": "", "email": "", "plan": "free"}

    def fill(self, field: str, value: str) -> None:
        self._values[field] = value

    def capture(self) -> FormSnapshot:
        return FormSnapshot(tuple(self._values.items()))

    def reset_to(self, snapshot: FormSnapshot) -> None:
        self._values = dict(snapshot._values)

    def show(self) -> None:
        print("  form:", self._values)


class UndoStack:
    def __init__(self) -> None:
        self._items: list[FormSnapshot] = []

    def push(self, snapshot: FormSnapshot) -> None:
        self._items.append(snapshot)

    def pop(self) -> FormSnapshot | None:
        return self._items.pop() if self._items else None


form = SignupForm()
undo = UndoStack()

undo.push(form.capture())
form.fill("name", "Kim")
undo.push(form.capture())
form.fill("plan", "pro")
form.show()

snapshot = undo.pop()
if snapshot is not None:
    form.reset_to(snapshot)
form.show()
OutputRan with python:3.12-slim (Python 3.12.13)
  form: {'name': 'Kim', 'email': '', 'plan': 'pro'}
  form: {'name': 'Kim', 'email': '', 'plan': 'free'}
The undo restores the plan to free and keeps the name.

The undo returns the snapshot taken after the name was filled in, so the form keeps "Kim" and goes back to the free plan.

If UndoStack read snapshot._values, for example to show a preview, it would depend on how the form stores its fields. Renaming a field in SignupForm would then break the undo stack, which is the coupling the pattern exists to prevent. A preview belongs on the form, as a method that takes a snapshot.