「Memento」パターンとは?サンプルを踏まえてわかりやすく解説!【Java】

  • LINEで送る
「Memento」パターンとは?サンプルを踏まえてわかりやすく解説!

 本記事ではGoFのデザインパターンのプログラムの振る舞いに関するパターンの一つである「Memento」パターンを解説します。このパターンを一言で説明するならば、「オブジェクトのスナップショットを作成し、それを復元できるようにするパターン」と言えるでしょう。「Memento」パターンをサンプルを踏まえて解説します。

Mementoパターンとは

 Mementoパターンは、オブジェクトの状態を外部に保存し、後でその状態に戻せるようにするデザインパターンです。「元に戻す(Undo)」や「状態のスナップショットを取る」といった機能が必要な場面でよく使われます。

 例えば、WordやExcel、PowerPointなどで文章を打ち込んだ時を想像してみてください。文章を打ち込んだ時に、間違えてたと思ったら「Ctrl + Z」で一つ前の状態に戻したりしませんか?これはまさしくMementoパターンといえます。操作ごとに現在の「状態のスナップショットを取る」、「Ctrl + Z」で「元に戻す(Undo)」ということをやっています。この「状態の保存と復元」をオブジェクト指向で表現するのがMementoパターンです。

Mementoパターンの構成要素

 Mementoパターンを実装する上で3つの構成要素が重要です。

要素役割
Originator(発起人)状態を持ち、その状態の保存・復元の責任を持つクラス
Memento(記録)保存される状態を保持するオブジェクト(外部から中身を直接操作できないようにする)
Caretaker(管理者)Mementoを保管し、必要に応じてOriginatorに渡して状態を復元させる

 本記事では、先ほどのなんらかのテキストを打ち込むとき(テキストエディター)の例で、実際のコードを上記の構成要素をもとに書いていきます。

Mementoパターンの書き方

❌ Mementoを使わない場合

 まずはMementoパターンを使わない場合、下記のような不具合につながる可能性があります。

  • 状態を「変数上書き」で管理すると、前の状態に戻せない
  • オブジェクト自体を書き換える必要があるためカプセル化が担保できず、オブジェクトの一貫性が損なわれる
  • 別のフィールドで前の状態を保持しておくと、処理が複雑になる可能性がある
  • 同じクラスで状態の保持と復元を行う必要があるため、本来classが持つ責務が曖昧になる

Mementoを使う場合

Mementoクラス

 それでは実際にテキストエディターの例でMementoパターンを実装していきます。まずは打ち込まれたテキストを記録するためのMementoクラスを実装します。

public class Memento {
    private String text;

    public Memento(String text) {
        this.text = text;
    }

    public String getText() {
        return text;
    }
}

 このクラスは、テキストを保持しておくためだけのクラスで、このクラスをStackに出し入れして状態の履歴を保持します。

Originatorクラス

 次にOriginatorに該当するクラスを作ります。テキストの保存、取得、復元等を行うTextEditorクラスを作成します。

public class TextEditor {
    private String text = "";

    public String getText() {
        return text;
    }

    // 現在の状態を保存する(Mementoを返す)
    public Memento save(){
        return new Memento(text);
    }

    // 保存された状態に復元する
    public void restore(Memento memento){
        this.text = memento.getText();
    }

    // 文字列を追加入力する(例:「こんにちは」と打つ)
    public void type(String newText) {
        this.text += newText;
    }
}

 getter以外のメソッドについての責務は下記になります。

メソッド責務
Memento save()現在の状態をMementoに保存する
void restore(Memento memento)保存された状態に戻す(Undo)
void type(String newText)状態を更新する

 Originatorは状態を操作します。今回のTextEditorクラスの状態は、フィールドのtextになります。

Caretakerクラス

 次にCaretakerに該当するクラスを実装します。今回はそのままCaretakerのクラス名として実装します。

public class Caretaker {
    private final Stack<Memento> history = new Stack<>();

    public void save(TextEditor textEditor) {
        history.push(textEditor.save());
    }

    public void undo(TextEditor textEditor) {
        if (!history.isEmpty()) {
            history.pop();
            Memento memento = history.isEmpty() ? null : history.peek();
            textEditor.restore(memento);
        }
    }
}

 このクラスはTextEditorで編集した値を保存(save)し、Undo操作で過去の状態に戻す(undo)操作を担います。Caretakerは過去の状態の履歴をもつ責務があります。

 状態の履歴を持つフィールドとして『Stack<Memento> history』を持っています。このコレクションはListとは違い、後入れ先出しLIFO(Last-In, First-Out)の原則をもとに動きます。重ねてあるお皿を上から順番に使うイメージです。後から重ねられたお皿ほど先に使われます。

 save()ではMementoをStackに入れていきます。undo()ではStackが空ではないことを確認後、pop()メソッドとpeek()メソッドを利用して、最後に追加したMementoのオブジェクトをStackから取り出します。その後restore()を利用して、復元されたMementoでtextの値を上書きします。

実際にMementoパターンを動かしてみる

 では実際にこれらのMementoを利用して文字列を操作してみます。

public class MementoSample {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();
        Caretaker caretaker = new Caretaker();

        editor.type("私は");
        caretaker.save(editor);

        editor.type("朝ごはんを");
        caretaker.save(editor);

        editor.type("食べました");
        caretaker.save(editor);
        System.out.println(editor.getText());

        caretaker.undo(editor);
        caretaker.undo(editor);

        editor.type("晩御飯を");
        caretaker.save(editor);
        editor.type("食べていません");
        caretaker.save(editor);

        System.out.println(editor.getText());
    }
}
>> 実行結果
私は朝ごはんを食べました
私は晩御飯を食べていません

 最終的にundoしながら「私は朝ごはんを食べました」から「私は晩御飯を食べていません」に書きかわりました。この実行結果は下記の手順で置き換わっています。

1「私は」をsave[“私は”]
2「朝ごはんを」をsave[“朝ごはんを”, “私は”]
3「食べました」をsave[“食べました”, “朝ごはんを”, “私は”]
4標準出力
「私は朝ごはんを食べました」
5undo[“朝ごはんを”, “私は”]
6undo[“私は”]
7「晩御飯を」をsave[“晩御飯を”, “私は”]
8「食べていません」をsave[“食べていません”, “晩御飯を”, “私は”]
9標準出力
「私は晩御飯を食べていません」

メリットとデメリット

メリット

メリット説明
状態の履歴を簡単に保存・復元できるUndoやRedoなどの実装が容易になる
状態の保存ロジックを他と分離できるMementoが状態を持つため、Originatorの責任が明確化される
元の状態に戻す処理が直感的save()restore() だけで管理可能

デメリット

デメリット説明
保存する状態が多いとメモリを消費する大量のMementoを保存するとパフォーマンス低下の要因に
状態の内部構造が複雑だと管理しにくいカプセル化を守るために、Mementoが非公開になると保守が難しくなる場合がある

 Mementoパターンには上記のメリットとデメリットがあります。これらのメリットとデメリットからMementoパターンが有効なシチュエーションには下記のような場合があります。

  • テキストエディタ、図形描画アプリ、フォーム入力等でUndo/Redo機能の実装が必要な時
  • ゲームのセーブ&ロード、設定画面の「元に戻す」ボタン等で状態のスナップショットを保存したい時
  • パズル・AIの探索アルゴリズムでのバックトラック、失敗時のロールバック処理を行いたいような、試行錯誤を伴う処理を実装したい時
  • ドキュメント操作履歴等の可逆的な操作の履歴を残したい時

まとめ

 Mementoパターンは「状態を保存して復元する」ためのデザインパターンで、UndoやRedoなどの機能を持つアプリケーションに最適です。
 今回のテキストエディタの例のように、編集状態を簡単に戻せる仕組みを安全かつシンプルに構築できます。

 本記事で利用したサンプルケースのクラス図は下記になります。

 最後に「Memento」パターンとは「オブジェクトのスナップショットを作成し、それを復元できるようにするパターン」です。うまく使っていきましょう。

 本記事で利用したコードは下記のgit上で公開しております。

https://github.com/tamotech1028/memento

最新の投稿

SNSでもご購読できます。

コメント

コメントを残す