cv.cm

コーディング · デザインパターン

Memento パターンとは?例でわかりやすく解説

約9分で読めます

結論

Memento は振る舞いに関するデザインパターンで、オブジェクトの内部状態を Memento と呼ばれるスナップショットオブジェクトに記録し、後でその状態に復元できるようにします。状態を持つオブジェクト(Originator)が Memento を作成して復元し、別のオブジェクト(Caretaker)はそれを保管するだけで、中身は決して見ません。カプセル化を壊さずに、Undo、チェックポイント、セーブデータを実現する標準的な方法です。

目的と解決する問題

目的:カプセル化を侵害せずに、オブジェクトの内部状態を捉えて外部に保存し、後でそのオブジェクトをその状態に復元できるようにすることです。Memento は Gang of Four(GoF)の書籍にある振る舞いパターンの 1 つです。

お絵かきアプリで、図形が位置、サイズ、色、そして描画に使うプライベートな導出値のリストを持っているとします。Undo ボタンを付けたいとき、Undo 管理がその図形のフィールドを自分でコピーすることもできますが、その場合はすべてのプライベートフィールドを知っていなければならず、図形クラスが変わるたびに Undo 管理が壊れます。Undo のためだけにすべてのフィールドを公開すると、どんなコードでも図形を不正な状態にできてしまいます。

Memento は、図形自身にスナップショットを作らせることでこれを解決します。スナップショットの中身と、そこからの復元方法を知っているのは図形だけです。Undo 管理はスナップショットを不透明なオブジェクトとして扱い、リストに保存して、ユーザーが Undo を押したときに 1 つ返します。

構造と登場する役割

役割責務
Originator状態が保存されるオブジェクト。現在の状態の Memento を作成し、Memento から自分を復元する
MementoOriginator の状態の不変なスナップショット。ほかのオブジェクトには役に立つものを何も公開しない
Caretakerいつ保存し、いつ復元するかを決め、Memento を(たとえばスタックに)保管するが、中身を読んだり変更したりは決してしない

重要なのは 2 つのインターフェースです。保存された状態を読める Originator 向けの広いインターフェースと、それ以外の全員向けの、Memento を保持することしかできない狭いインターフェースです。

TypeScript での実装

Memento クラスは JavaScript のプライベートフィールド(#state)を使うので、Caretaker は Memento オブジェクトから保存データを読めません。読み取りは静的メソッド EditorMemento.read を通じて行われ、これを呼ぶのはエディタだけです。実際のプロジェクトでは、Memento クラスをエディタと同じモジュールに置いてエクスポートしないようにし、ほかのモジュールは Memento を保持できても、開くことはできないようにします。

type EditorState = { text: string; cursor: number };

class EditorMemento {
  readonly #state: EditorState;
  constructor(state: EditorState) {
    this.#state = { ...state }; // copy, so later edits do not change the snapshot
  }
  static read(m: EditorMemento): EditorState {
    return { ...m.#state };
  }
}

class Editor {
  private state: EditorState = { text: "", cursor: 0 };
  type(words: string) {
    const text = this.state.text + words;
    this.state = { text, cursor: text.length };
  }
  get text() { return this.state.text; }
  save(): EditorMemento { return new EditorMemento(this.state); }
  restore(m: EditorMemento) { this.state = EditorMemento.read(m); }
}

class History {
  private stack: EditorMemento[] = [];
  constructor(private editor: Editor) {}
  backup() { this.stack.push(this.editor.save()); }
  undo() { const m = this.stack.pop(); if (m) this.editor.restore(m); }
}

const editor = new Editor();
const history = new History(editor);
history.backup(); editor.type("Hello");
history.backup(); editor.type(" world");
console.log(editor.text); // Hello world
history.undo();
console.log(editor.text); // Hello
history.undo();
console.log(JSON.stringify(editor.text)); // ""

History が Caretaker です。変更の前に backup() を、元に戻すときに undo() を呼びますが、text や cursor を見ることはありません。あとで EditorState に選択範囲を追加しても、History は変更不要です。

Undo における Memento と Command の比較

Undo の実装には一般的な方法が 2 つあり、組み合わせて使われることもよくあります。

方式保存するもの向いている場合コスト
Memento変更の前の、状態全体のスナップショット変更を元に戻すのが難しい場合や、状態が小さい場合メモリは、状態のサイズに手順の数を掛けた分だけ増える
Command操作と、それを元に戻すのに十分なデータ各操作に単純な逆操作がある場合各コマンドに正しい Undo のロジックが必要

現実的な折衷案は、ほとんどの手順ではコマンドを保存し、N 手順ごとに完全な Memento を保存する方法や、コマンドが変更する状態の一部だけを保存する方法です。

使うべき場面と避けるべき場面

  • Undo、Redo、チェックポイント、セーブデータ、そして編集内容を巻き戻したいフォームの「キャンセル」ボタンに使います。
  • メモリ内のトランザクションに使います。危険な操作の前にスナップショットを取り、失敗したら復元します。
  • Caretaker を Originator の内部フィールドに依存させたくないときに使います。
  • 状態が非常に大きく頻繁に変わる場合は、履歴の長さを制限するか差分だけを保存しない限り、使いません。
  • 状態がすでに単なる不変の値であれば、大げさな仕組みは不要です。不変データなら、古い値がそのままスナップショットなので、以前の値のリストを保持すれば十分です。

実際の利用例

  • ブラウザの History API:history.pushState(state, "", url) は履歴の各エントリに状態オブジェクトを保存し、ユーザーが移動したときに popstate イベントがそれを返します。
  • Redux などのストアは不変な状態のスナップショットを保持し、ツールがそれをタイムトラベルデバッグに利用します。
  • SQL データベースはトランザクション内で SAVEPOINT をサポートしており、名前を付けた地点まで作業を巻き戻せます。
  • テキストエディタ、グラフィックスソフト、ゲームは、Undo、自動保存、セーブスロットのためにスナップショットを保存します。

関連するパターン

パターンMemento パターンとの関係
Command パターンCommand は操作を逆にして Undo できます。実行前に取った Memento を保存し、undo でそれを復元することもできます。
Prototype パターンPrototype はオブジェクト全体を複製して新しいオブジェクトを作ります。Memento は、同じオブジェクトを後で復元するために状態を保存します。
Iterator パターンIterator は現在位置を Memento として公開でき、走査を後で再開できます。
State パターンState は、状態が変わるとオブジェクトの振る舞いを変えます。Memento はデータを記録して復元するだけです。

よくある質問

Originator と Caretaker の違いは何ですか?

Originator は状態を持ち、Memento を作成し、そこから復元できる唯一のオブジェクトです。Caretaker はいつ保存・復元するかを決めて Memento を保管しますが、それらを不透明なものとして扱います。

Memento はカプセル化を壊しませんか?

壊しません。それがこのパターンの目的です。Originator の内部フィールドはプライベートのままで、Memento がそれらを Caretaker から隠すので、保存された状態を読めるのは Originator だけです。

Memento のメモリ使用量を抑えるにはどうすればよいですか?

スナップショットの最大数を決めて古いものから捨てる、状態の変更部分だけを保存する、あるいは小さな変更を記録するコマンドと Memento を組み合わせる、といった方法があります。

JSON へのシリアライズは Memento ですか?

状態を JSON にシリアライズすることは、Memento の保存形式になり得ますし、ディスクへの保存にも便利です。このパターンが扱うのは、誰がスナップショットを作り、保管し、復元するかであり、形式ではありません。

コーディング講座の一覧