広告

Java : Memento パターン (図解/デザインパターン)

Memento パターンとは、GoF によって定義されたデザインパターンの1つです。
オブジェクトの 非公開(private) な内部状態を保存・復元します。

本記事では、Memento パターンを Java のコード付きで解説していきます。


デザインパターン(GoF) 関連記事

概要

memento パターン(英: Memento pattern、日本: メメント パターン)はソフトウェアのデザインパターンの一つで、オブジェクトを以前の状態に(ロールバックにより)戻す能力を提供する。

Memento パターンとは、

  • オブジェクトの 非公開(private) な内部状態を保存・復元する

という目的のための設計です。

Memento は、日本語的に発音すると「メメント」となります。
意味は「思い出の品」や「形見」ですね。


【Memento パターンのイメージ】

イメージ図1

ある1つのオブジェクトがあるとします。
そのオブジェクトは、いくつかの属性による 内部状態 を持ちます。

ただし、外側からはその内部状態にアクセスできません。
つまり 内部状態は 隠蔽(カプセル化) されているわけですね。

例えば次のようなクラスです。

public class A {
    private int n1;
    private int n2;

    public void increment() {
        n1 += 1;
        n2 += 2;
    }

    public void print() {
        System.out.println("n1 = " + n1 + " : n2 = " + n2);
    }
}

クラスA の内部状態は、private フィールドの n1n2 によって表されます。
しかし、n1n2 を取得するためのメソッドはありません。


イメージ図2

Memento パターンを使うと、オブジェクトの内部状態を「メメント・オブジェクト」として 生成 できます。
そして、その「メメント・オブジェクト」を使うと、オブジェクトをその時の状態へと 復元 できます。

例えば

  • 元に戻す(アンドゥ)
  • やり直し(リドゥ)

といった機能の実現に使えますね。

関連記事 :


クラス図とシーケンス図

クラス図1

シーケンス図1

上の図は、Memento パターンの一般的なクラス図とシーケンス図です。
それでは、このクラス図をそのままコードにしてみましょう。

まずは、Originator と Memento クラスです。
Memento クラスは、Originator の 内部クラス として定義します。

public class Originator {
    private String state;

    public void setState(String state) {
        this.state = state;
    }

    public Memento save() {
        return new Memento(state);
    }

    public void restore(Memento memento) {
        state = memento.getState();
    }

    @Override
    public String toString() {
        return "state = " + state;
    }

    public static class Memento {
        private final String state;

        private Memento(String state) {
            this.state = state;
        }

        private String getState() {
            return state;
        }
    }
}

Memento.getState メソッドは 非公開(private) とします。
つまり、外部からはアクセスできません。

これにより、Originator の内部状態の 詳細 を外部に公開しなくて済むわけですね。

次に、Caretaker クラスです。

public class Caretaker {
    private Originator.Memento memento;

    public void save(Originator originator) {
        memento = originator.save();
    }

    public void restore(Originator originator) {
        if (memento == null) {
            return;
        }

        originator.restore(memento);
    }
}

Originator オブジェクトを使い、メメントを保存・復元します。

それでは、これらのクラスを使ってみましょう。

final var originator = new Originator();
final var caretaker = new Caretaker();

originator.setState("A");
System.out.println(originator); // state = A

caretaker.save(originator);

originator.setState("B");
System.out.println(originator); // state = B

caretaker.restore(originator);
System.out.println(originator); // state = A

結果も問題なしですね。
save メソッドで保存した内部状態を、restore メソッドで復元できました。


具体的な例

もう少し具体的な例も見てみましょう。

クラス図2

足し算だけができる、とてもシンプルな計算機(Calc) を考えてみます。

  1. start メソッド : 計算を開始
  2. add メソッド : 足し算
  3. getResult メソッド : 結果として 計算式 を取得

という流れになります。

public class Calc {
    private StringBuilder expression;
    private int value;

    public void start(int value) {
        this.value = value;
        expression = new StringBuilder();
        expression.append(value);
    }

    public void add(int value) {
        this.value += value;
        expression.append(" + ").append(value);
    }

    public String getResult() {
        return expression.toString() + " = " + value;
    }
}

各フィールドの役割は次のようになります。

  • expression フィールド : 計算式を文字列で保存
  • value フィールド : 足し算した結果の値

expression には、変更可能な文字列として StringBuilder クラスを使っています。

それでは Calc クラスを使ってみましょう。

final var calc = new Calc();

calc.start(10);
calc.add(20);
calc.add(30);

// 10 + 20 + 30 = 60
System.out.println(calc.getResult());

結果も問題なしですね。
無事に足し算ができました。

Memento パターンを使う例

クラス図3

先ほどの Calc クラスに対して「元に戻す(アンドゥ)」機能を追加してみましょう。

まずは Calc クラスに

  • save メソッド : 内部状態をメメントに保存
  • restore メソッド : メメントから内部状態を復元
  • Memento 内部クラス

を追加します。

public class Calc {
    ... 省略 ...

    public Memento save() {
        return new Memento(expression.toString(), value);
    }

    public void restore(Memento memento) {
        this.expression = new StringBuilder(memento.getExpression());
        this.value = memento.getValue();
    }

    public static class Memento {
        private final String expression;
        private final int value;

        private Memento(String expression, int value) {
            this.expression = expression;
            this.value = value;
        }

        private String getExpression() {
            return expression;
        }

        private int getValue() {
            return value;
        }
    }
}

次は Caretaker クラスです。
今回は Proxy パターン の Proxy のような役割になっています。

public class Caretaker {
    private final Deque<Calc.Memento> history = new ArrayDeque<>();
    private final Calc calc;

    public Caretaker(Calc calc) {
        this.calc = calc;
    }

    public void start(int value) {
        history.clear();

        calc.start(value);
    }

    public void add(int value) {
        final var memento = calc.save();
        history.addLast(memento);

        calc.add(value);
    }

    public String getResult() {
        return calc.getResult();
    }

    public void undo() {
        if (history.isEmpty()) {
            return;
        }

        final var memento = history.removeLast();
        calc.restore(memento);
    }
}

history フィールドは「メメント」オブジェクトを保存する 両端キュー になります。

  1. start メソッド : history キューをクリア
  2. add メソッド : history キューに「メメント」を追加
  3. undo メソッド : 最後に追加した「メメント」を history キューから取り出し、Calc オブジェクトの内部状態を復元
  4. getResult メソッド : 結果となる計算式を取得

という流れになります。

それでは、これらのクラスを使ってみましょう。

final var caretaker = new Caretaker(new Calc());

caretaker.start(10);
caretaker.add(20);
caretaker.add(30);
caretaker.add(40);

// 10 + 20 + 30 + 40 = 100
System.out.println(caretaker.getResult());

caretaker.undo();

// 10 + 20 + 30 = 60
System.out.println(caretaker.getResult());

caretaker.undo();

// 10 + 20 = 30
System.out.println(caretaker.getResult());

caretaker.add(5);

// 10 + 20 + 5 = 35
System.out.println(caretaker.getResult());

結果も問題なしですね。
無事に「元に戻す(アンドゥ)」も機能しました。

まとめ

Memento パターンとは、

  • オブジェクトの 非公開(private) な内部状態を保存・復元する

という目的のための設計です。

  • 元に戻す(アンドゥ)
  • やり直し(リドゥ)

といった機能を実装するときには、Memento パターンを検討してみましょう。


関連記事

ページの先頭へ