概要

オブジェクトの内部状態をカプセル化を崩さずに保存し、後から復元できるようにする。Memento パターンは、テキストエディタの Undo、ゲームのセーブデータ、設定のロールバックなど、「元に戻す」操作が必要な場面で使われます。Command パターンが操作をオブジェクト化するのに対し、Memento は状態そのものをスナップショットとして保存する点が異なります。この記事では、テキストエディタの内容とカーソル位置を Memento に保存し、Deque で履歴管理して Undo を実現する実装を示します。Java 17 の record は不変で equals/hashCode が自動生成されるため、Memento の実装に最適です。Java 21 では sealed interface で編集状態のバリエーションを型安全に表現できます。

使いどころ

テキストエディタの内容とカーソル位置をスナップショットとして保存し、Undo で任意の時点に戻す

設定画面の変更を適用前にスナップショットとして保存し、キャンセル時に元の設定に復元する

ゲームの進行状態をセーブポイントとして保存し、ゲームオーバー時にセーブポイントから再開する

コード例

MementoPatternDemo.java
import java.util.ArrayDeque;
import java.util.Deque;

public class MementoPatternDemo {

    record EditorMemento(String content, int cursorPos) {}

    static class TextEditor {
        private String content = "";
        private int cursorPos = 0;

        void type(String text) {
            content = content.substring(0, cursorPos)
                    + text + content.substring(cursorPos);
            cursorPos += text.length();
        }

        EditorMemento save() {
            return new EditorMemento(content, cursorPos);
        }

        void restore(EditorMemento memento) {
            this.content = memento.content();
            this.cursorPos = memento.cursorPos();
        }

        void display() {
            System.out.println("内容: "" + content
                + "" (カーソル: " + cursorPos + ")");
        }
    }

    static class EditorHistory {
        private final Deque<EditorMemento> undoStack
                = new ArrayDeque<>();

        void save(EditorMemento memento) {
            undoStack.push(memento);
        }

        EditorMemento undo() {
            if (undoStack.isEmpty()) return null;
            undoStack.pop();
            if (!undoStack.isEmpty()) {
                return undoStack.peek();
            }
            return new EditorMemento("", 0);
        }

        boolean canUndo() {
            return !undoStack.isEmpty();
        }
    }

    public static void main(String[] args) {
        var editor = new TextEditor();
        var history = new EditorHistory();

        history.save(editor.save());
        editor.type("Hello");
        editor.display();

        history.save(editor.save());
        editor.type(" World");
        editor.display();

        history.save(editor.save());
        editor.type("!!!");
        editor.display();

        System.out.println("--- Undo ---");
        if (history.canUndo()) {
            var prev = history.undo();
            if (prev != null) editor.restore(prev);
        }
        editor.display();

        if (history.canUndo()) {
            var prev = history.undo();
            if (prev != null) editor.restore(prev);
        }
        editor.display();
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record で Memento を定義すると、不変性・equals/hashCode・toString が自動で得られる。Memento 実装に最適。

Java 17
// Java 17: record で Memento を簡潔に定義
// 不変性・equals・hashCode・toString が自動生成
record EditorMemento(String content, int cursorPos) {}

// 利用側: record のアクセサで復元
void restore(EditorMemento memento) {
    this.content = memento.content();
    this.cursorPos = memento.cursorPos();
}

Library Comparison

標準 API(record + Deque)スナップショットの保存と復元を自前で管理する場面。外部依存なしで完結する。差分保存やシリアライズが必要になると自前コードが増える。
javax.swing.undo.UndoableEditSwing テキストコンポーネントの Undo/Redo。Command + Memento の組み合わせで実装されている。Swing 前提のため、Web やバッチ処理では利用できない。
Serializable によるバイト列保存オブジェクト全体をバイト列として保存し、ファイルや DB に永続化する場合。シリアライズのオーバーヘッドが大きく、頻繁な Undo 操作には向かない。バージョン互換の問題もある。

注意点

スナップショットを大量に保持するとメモリを圧迫する。履歴件数の上限を設けるか、古いスナップショットを定期的に破棄する設計にする

Memento の中身を Caretaker が直接読み書きしないこと。Caretaker はスナップショットの保管と受け渡しだけを担い、内容には触れない

Memento にミュータブルなオブジェクト(List や Map)を含める場合は、保存時にディープコピーを取ること。参照のままだと復元しても元の状態に戻らない

record で Memento を定義すると toString() で内部状態が露出する。セキュリティ上の懸念がある場合は toString() をオーバーライドするか、通常のクラスで定義する

FAQ

Memento パターンと Command パターンの Undo はどう違いますか。

Command は操作の逆操作で Undo します。Memento は操作前の状態を丸ごと保存して復元します。Memento の方がシンプルですが、状態が大きいとメモリコストが高くなります。

record の Memento で equals が自動生成されるメリットは何ですか。

同じ内容のスナップショットかどうかをフィールド値で比較でき、重複保存の検知やテストでの比較が容易になります。

差分だけ保存する方法はありますか。

操作前後の差分をコマンドオブジェクトとして保存する方法が一般的です。これは Command パターンの Undo と同等の設計になります。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます