Java : Memento パターン (図解/デザインパターン)
Memento パターンとは、GoF によって定義されたデザインパターンの1つです。
オブジェクトの 非公開(private) な内部状態を保存・復元します。
本記事では、Memento パターンを Java のコード付きで解説していきます。
デザインパターン(GoF) 関連記事
- 生成に関するパターン
- 振る舞いに関するパターン
概要
Memento パターンとは、
- オブジェクトの 非公開(private) な内部状態を保存・復元する
という目的のための設計です。
Memento は、日本語的に発音すると「メメント」となります。
意味は「思い出の品」や「形見」ですね。
【Memento パターンのイメージ】
ある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 フィールドの n1 と n2 によって表されます。
しかし、n1 と n2 を取得するためのメソッドはありません。
Memento パターンを使うと、オブジェクトの内部状態を「メメント・オブジェクト」として 生成 できます。
そして、その「メメント・オブジェクト」を使うと、オブジェクトをその時の状態へと 復元 できます。
例えば
- 元に戻す(アンドゥ)
- やり直し(リドゥ)
といった機能の実現に使えますね。
関連記事 :
クラス図とシーケンス図
上の図は、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 メソッドで復元できました。
具体的な例
もう少し具体的な例も見てみましょう。
足し算だけができる、とてもシンプルな計算機(Calc) を考えてみます。
- start メソッド : 計算を開始
- add メソッド : 足し算
- 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 パターンを使う例
先ほどの 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 フィールドは「メメント」オブジェクトを保存する 両端キュー になります。
- start メソッド : history キューをクリア
- add メソッド : history キューに「メメント」を追加
- undo メソッド : 最後に追加した「メメント」を history キューから取り出し、Calc オブジェクトの内部状態を復元
- 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 パターンを検討してみましょう。
関連記事
- 標準APIにならう命名規則
- コメントが少なくて済むコードを目指そう
- シングルトン・パターンの乱用はやめよう
- メソッドのパラメータ(引数)は使う側でチェックしよう
- 不変オブジェクト(イミュータブル) とは
- 依存性の注入(DI)をもっと気軽に
- 不要になったコードはコメントアウトで残さずに削除しよう
- 簡易的な Builder パターン
- 読み取り専用(const) のインタフェースを作る
- 図解/デザインパターン一覧 (GoF)
- Abstract Factory パターン
- Adapter パターン
- Bridge パターン
- Builder パターン
- Chain of Responsibility パターン
- Command パターン
- Composite パターン
- Decorator パターン
- Facade パターン
- Factory Method パターン
- Flyweight パターン
- Interpreter パターン
- Iterator パターン
- Mediator パターン
- Observer パターン
- Prototype パターン
- Proxy パターン
- Singleton パターン
- State パターン
- Strategy パターン
- Template Method パターン
- Visitor パターン