Concepts / Behavioral patterns / Memento and undo

Memento and undo

Saving and restoring an object's state without breaking encapsulation.

01The intuition

The problem it solves

A game of 2048 should let the player undo a move. The grid is private inside the Board class, as it should be. The obvious undo makes the history list reach in and copy board._cells directly, so now two classes depend on the grid's private layout, and the day the grid becomes a flat array the history code breaks too.

class Board:
    def __init__(self) -> None:
        self._cells = [[0] * 4 for _ in range(4)]
    def slide(self, direction: str) -> None: ...

class History:
    def __init__(self, board: Board) -> None:
        self.board = board
        self.past: list[list[list[int]]] = []
    def save(self) -> None:
        self.past.append([row[:] for row in self.board._cells])   # reaches inside
    def undo(self) -> None:
        self.board._cells = self.past.pop()                        # and again

Build it one piece at a time

Step one: let the board itself produce a sealed copy of its state, and accept such a copy back. The copy is a small object that only the board knows how to read. Programmers call the copy a memento.

from dataclasses import dataclass

@dataclass(frozen=True)
class BoardMemento:
    cells: tuple[tuple[int, ...], ...]     # immutable, opaque to everyone else
    score: int

class Board:
    def save(self) -> BoardMemento:
        return BoardMemento(tuple(tuple(r) for r in self._cells), self.score)
    def restore(self, m: BoardMemento) -> None:
        self._cells = [list(r) for r in m.cells]
        self.score = m.score

Step two: the history becomes a caretaker. It stores mementos and hands them back; it never looks inside one. The board's private layout can change freely as long as save and restore agree.

class History:
    def __init__(self, board: Board) -> None:
        self.board = board
        self.past: list[BoardMemento] = []
        self.future: list[BoardMemento] = []

    def record(self) -> None:
        self.past.append(self.board.save()); self.future.clear()

    def undo(self) -> None:
        if self.past:
            self.future.append(self.board.save())
            self.board.restore(self.past.pop())

    def redo(self) -> None:
        if self.future:
            self.past.append(self.board.save())
            self.board.restore(self.future.pop())

Step three: a move is now three lines in the game loop: record, slide, redraw. Because mementos are immutable, keeping fifty of them is safe; nothing can edit an old one by mistake. The game loop never learns what a board is made of, and a test can assert that save followed by restore round-trips any position.

history.record()
board.slide("left")
screen.draw(board)

Why it works

The principle: the object that owns the state is the only one allowed to read or write it, so it is also the only one that may package it up for later. The invariant is that restore(save()) leaves the object exactly as it was, and a memento, once made, never changes. Where it breaks: a memento that holds references to mutable things (a list the board keeps editing) so the snapshot changes after the fact; a caretaker that peeks into mementos; and snapshots of a state that includes things that should not roll back, such as a running clock or an already-sent message. Interviewers test with "undo after redo after a new move" and with memory: a thousand snapshots of a big state.

The trade-off you must say out loud

Snapshots are simple and always correct, but they cost memory proportional to the state size per step. When the state is large and changes are small, a reversible command (store the diff) is cheaper, and the honest answer is often a mix: a memento every twenty moves plus commands in between. The follow-up is usually exactly that: memento versus command, and how to cap the history.

Where it shows up in the 100 Google problems

02Used in these problems