Card 07/ 08
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.
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()- Which class is the originator, which the memento, and which the caretaker?
- Which classes read the saved field values, and which only pass a snapshot along?
- What does the form show after the undo?
- Python does not enforce the underscore in
_values. What would break the pattern ifUndoStackignored it?
Show a solution
| Class | Participant | Reads the saved values |
|---|---|---|
SignupForm | Originator | Yes, in reset_to |
FormSnapshot | Memento | Holds them, as _values |
UndoStack | Caretaker | No; it only pushes and pops snapshots |
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 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.
← → move through sections and cards