概要
オブジェクトの内部状態をカプセル化を崩さずに保存し、後から復元できるようにする。Memento パターンは、テキストエディタの Undo、ゲームのセーブデータ、設定のロールバックなど、「元に戻す」操作が必要な場面で使われます。Command パターンが操作をオブジェクト化するのに対し、Memento は状態そのものをスナップショットとして保存する点が異なります。この記事では、テキストエディタの内容とカーソル位置を Memento に保存し、Deque で履歴管理して Undo を実現する実装を示します。Java 17 の record は不変で equals/hashCode が自動生成されるため、Memento の実装に最適です。Java 21 では sealed interface で編集状態のバリエーションを型安全に表現できます。
使いどころ
テキストエディタの内容とカーソル位置をスナップショットとして保存し、Undo で任意の時点に戻す
設定画面の変更を適用前にスナップショットとして保存し、キャンセル時に元の設定に復元する
ゲームの進行状態をセーブポイントとして保存し、ゲームオーバー時にセーブポイントから再開する
コード例
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();
}
}Version Coverage
record で Memento を定義すると、不変性・equals/hashCode・toString が自動で得られる。Memento 実装に最適。
// 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
注意点
スナップショットを大量に保持するとメモリを圧迫する。履歴件数の上限を設けるか、古いスナップショットを定期的に破棄する設計にする
Memento の中身を Caretaker が直接読み書きしないこと。Caretaker はスナップショットの保管と受け渡しだけを担い、内容には触れない
Memento にミュータブルなオブジェクト(List や Map)を含める場合は、保存時にディープコピーを取ること。参照のままだと復元しても元の状態に戻らない
record で Memento を定義すると toString() で内部状態が露出する。セキュリティ上の懸念がある場合は toString() をオーバーライドするか、通常のクラスで定義する
FAQ
Command は操作の逆操作で Undo します。Memento は操作前の状態を丸ごと保存して復元します。Memento の方がシンプルですが、状態が大きいとメモリコストが高くなります。
同じ内容のスナップショットかどうかをフィールド値で比較でき、重複保存の検知やテストでの比較が容易になります。
操作前後の差分をコマンドオブジェクトとして保存する方法が一般的です。これは Command パターンの Undo と同等の設計になります。