Mementoパターン


 ★☆☆

Mementoパターン

インスタンスの、あるときの状態を保存しておく

Memento とは、英語で「記念品」「形見」を意味する単語です。Memento パターンとは、 インスタンスのあるときの状態をスナップショットとして保存しておくことで、 その時のインスタンスの状態を復元することを可能にするものです。

インスタンスの状態が、プログラム実行中にどんどん変化することが考えられます。一度変化してしまったインスタンスを、「少し前の状態に戻したい」「ある時点の状態に戻したい」などの要求は時に発生するものです。このような要求にスマートに応えることができるのが、Memento パターンです。


例えば、ゴルフで空振りをしたとかOBを出してしまったというようなとき、今までのはなかったことにして、さっきのところからもう一度やり直そうというようなものです。

Memento パターンを使うと、インスタンスのある時の状態を、簡単にスナップショットとして残すことができ、さらに、そこからの復元も可能になります。インスタンス全ての状態を覚えておくために、clone を作成することもありますが、Memento パターンでは、必要な情報のみを保持しておき、必要なデータのみを復元することを考えます。

clone を作成する方法にはいくつか問題があります。このメソッドの返り値の型は Object となってしまうため、厳密な型チェックができません。また、clone() メソッドは基本的に複製を作成するためのものであるため、すべてのフィールドをコピーするように実装するのが普通です。このため、一部の情報だけを複製したオブジェクトを作成したいという用途には向いていません。

一部の情報だけを複製したオブジェクトを作成するためには、保存したい情報をパラメータとして受け取るコンストラクタを用意するのが一般的です。しかし、保存したい情報が増えるとパラメータ数も増えてしまい、使い勝手の悪いコンストラクタとなってしまいます。

また、実際にそのようなコンストラクタを用意したとしても、パラメータとして渡す情報はコピー元のオブジェクトから取得する必要があるため、コピー元となるオブジェクトのクラスは、情報を提供することになります。このことは、ある意味では内部構造を公開しているのと同じことになるため、カプセル化の破壊をしているということになります。

こういったことを考えていくと、カプセル化の破壊を防ぐための工夫をしつつ、保存したい情報を表現する特別なクラスを定義し、このクラスのオブジェクトを使うことにより、状態を保存したり、復元したり、といったことができるようにすれば良さそうだということになります。この方法を具体的に実現するデザインパターンが Mementoです。


情報を見せる、見せないの制御をするために、public、protected、private、あるいは何も書かないこと(パケージプライベート)によるアクセス制御を注意深く設計する必要がある点に注意してください。


例題

次のような木構造があります。これをたどってノード値の合計が100以上になるルートを見つけなさい。

木構造 t01  tnn:ノードの名前 (0) ()内はノード値 / \ t11 t12 (10) (20) /\ /\ t21 t22 t23 t24 (30) (20) (30) (20) / /\ /\ \ t31 t32 t33 t34 t35 t36 (40) (20)(30) (40)(10) (30) / / │ │ /\ t41 t42 t43 t44 t45 t46 (10) (20) (30) (20) (10) (20) /\ /\ t51 t52 t53 t54 (10) (20) (10) (20)

実行結果
t01 (0)    
t12 (20)
t23 (30)
t35 (10)
t44 (20)
t54 (20)







共通クラス

Node.java
public class Node { protected Node left = null; protected Node right = null; protected String name; protected int number = 0; public Node(String name, int number, Node left, Node right) { this.name = name; this.number = number; this.left = left; this.right = right; } public String getName() { return name; } public int getNumber() { return number; } }

Mementoパターンを使用しない例

Search.java
public class Search { public boolean exec(Node node, int total, List<Node> list) { total += node.number; list.add(node); if (total >= 100) return true; Node left = node.left; if (left != null) { boolean result = exec(left, total, list); if (result == true) { return result; } } Node right = node.right; if (right != null) { boolean result = exec(right, total, list); if (result == true) { return result; } } list.remove(node); return false; } }
Main.java
public class Main { public static void main(String[] args) { List<Node> list = new ArrayList<Node>(); Node t51 = new Node("t51", 10, null, null); Node t52 = new Node("t52", 20, null, null); Node t53 = new Node("t53", 10, null, null); Node t54 = new Node("t54", 20, null, null); Node t41 = new Node("t41", 10, null, null); Node t42 = new Node("t42", 20, t51, t52); Node t43 = new Node("t43", 30, null, null); Node t44 = new Node("t44", 20, t53, t54); Node t45 = new Node("t45", 10, null, null); Node t46 = new Node("t46", 20, null, null); Node t31 = new Node("t31", 40, t41, null); Node t32 = new Node("t32", 20, t42, null); Node t33 = new Node("t33", 30, t43, null); Node t34 = new Node("t34", 40, null, null); Node t35 = new Node("t35", 10, t44, null); Node t36 = new Node("t36", 30, t45, t46); Node t21 = new Node("t21", 30, t31, null); Node t22 = new Node("t22", 20, t32, t33); Node t23 = new Node("t23", 30, t34, t35); Node t24 = new Node("t24", 20, t36, null); Node t11 = new Node("t11", 10, t21, t22); Node t12 = new Node("t12", 20, t23, t24); Node t01 = new Node("t01", 0, t11, t12); Search s = new Search(); s.exec(t01, 0, list); Iterator<Node> it = list.iterator(); while (it.hasNext()) { Node node = (Node) it.next(); System.out.println(node.getName() + " (" + node.getNumber() + ")"); } } }

Mementoパターンを使用した例

Search.java
public class Search { public List<Node> exec(Node node) { List<Node> route = new ArrayList<Node>(); int total = 0; Memento memento = null; Stack<Memento> mementoStack = new Stack<Memento>(); while (true) { total += node.number; route.add(node); if (total >= 100) break; Node left = node.left; Node right = node.right; if (left != null && right != null) { memento = new Memento(node, total, route); mementoStack.push(memento); node = left; } else if (left == null && right == null) { if (mementoStack.empty()) return null; memento = (Memento) mementoStack.pop(); node = memento.getNode().right; route = memento.getRoute(); total = memento.getTotal(); } else if (left != null) node = node.left; else node = node.right; } return route; } }
Memento.java
public class Memento { private Node node; // ノード private int total; // 合計値 private ArrayList<Node> route; // ルート protected Memento(Node node, int total, List<Node> route) { this.node = node; this.total = total; this.route = new ArrayList<Node>(); for (int i = 0; i < route.size(); i++) this.route.add(route.get(i)); } protected Node getNode() { return node; } protected int getTotal() { return total; } protected List<Node> getRoute() { // ルートを得る return route; } }
Main.java
public class Main { public static void main(String[] args) { Node t51 = new Node("t51", 10, null, null); Node t52 = new Node("t52", 20, null, null); Node t53 = new Node("t53", 10, null, null); Node t54 = new Node("t54", 20, null, null); Node t41 = new Node("t41", 10, null, null); Node t42 = new Node("t42", 20, t51, t52); Node t43 = new Node("t43", 30, null, null); Node t44 = new Node("t44", 20, t53, t54); Node t45 = new Node("t45", 10, null, null); Node t46 = new Node("t46", 20, null, null); Node t31 = new Node("t31", 40, t41, null); Node t32 = new Node("t32", 20, t42, null); Node t33 = new Node("t33", 30, t43, null); Node t34 = new Node("t34", 40, null, null); Node t35 = new Node("t35", 10, t44, null); Node t36 = new Node("t36", 30, t45, t46); Node t21 = new Node("t21", 30, t31, null); Node t22 = new Node("t22", 20, t32, t33); Node t23 = new Node("t23", 30, t34, t35); Node t24 = new Node("t24", 20, t36, null); Node t11 = new Node("t11", 10, t21, t22); Node t12 = new Node("t12", 20, t23, t24); Node t01 = new Node("t01", 0, t11, t12); Search s = new Search(); List<Node> route = s.exec(t01); if (route != null) { Iterator<Node> it = route.iterator(); while (it.hasNext()) { Node node = (Node) it.next(); System.out.println(node.getName() + " (" + node.getNumber() + ")"); } } } }

この例では再起呼び出しで行っていますが、例えば t41 まで来て 100 にならなかった場合、t31、t21 というように順に戻りながら、他のルートを探すことになります。

しかし、t01 から t41 に来る間に、分岐は t11 のところにしかないと分かったのなら、一気に t11 に戻った方が効率がいいはずです。

Momento パターンでは、分岐に来た(left も right も null でない)ときに、その時点の情報の Memonto インスタンスを作り、スタックに保存しています。

Memento memento(node, total, route);

mementostack.push(memento);

そして、行き止まりになった(left も right も null)ときには、スタックから Memonto インスタンスを取り出しています。これは、直前の分岐地点の情報です。つまり、それまでのルートと合計、そして、これから行く右側ルートです。

memento = (Memento)mementoStack.pop();

node = memento.getNode().right; // 右側ルート

route = memento.getRoute(); // それまでのルート

total = memento.getTotal(); // それまでの合計