はじめに & 概要
デザインパターンの1つ、Compositeパターンに登場するオブジェクトをイミュータブルにしたら、循環参照を防ぐことができました。でも後で気づいたら、(多くの場合)全然使い物になりませんでした。
前知識
Compositeパターンについて(おさらい)
Compositeパターンは、ディレクトリ階層のように入れ子状の構造を表すために使われるデザインパターンです。GoFの23のデザインパターンでも取り上げられ、「容器と中身を同一視」することが特徴です。
上のクラス図では、Composite
クラスがフォルダーなどの入れ物を、Leaf
クラスがファイルなどのアイテムを示していて、Component
抽象クラスを使うことでこれらを同一視しています。Composite
はComponent
を要素として持ち、これによりLeaf
もComposite
も要素にすることが可能です。Composite
に要素を追加するにはadd(Component)
を使います。
Compositeパターンで発生する循環参照について
Composite
クラスは、生成後もadd(Component)
を呼び出すことができるため、循環参照が起きる可能性があります。
データベースの検索クエリーのようなものを考えて具体例で説明します。Query
インターフェースはjudge(Object)
抽象メソッドを持ち、オブジェクトがクエリーに該当するかどうかを判定します。KeyValueQuery
クラスは値とキーの組み合わせで表現されるクエリーです(おそらく、渡されたオブジェクトからkeyに対応する値を取り出し、valueと照合するのでしょう)。AndQuery
クラスは複数のQuery
を子要素として持ち、すべての子要素であるクエリーに該当するかどうかを判定します。
循環参照が発生する状況を示すため、さらに具体化したオブジェクト図を描いてみました。AのAndQuery
が「もも」と「千葉県」のKeyValueQuery
とBのAndQuery
を子要素として持っています。Bもまた子要素を持ち、CのAndQuery
はさらにAを子要素として持っています。
おわかりでしょうが、あるオブジェクトがAのAndQuery
に該当するかどうかを調べるため、A.judge()
を呼び出すと、BやCのjudge()
も連鎖的に呼び出され、またA.judge()
が呼び出されてしまいます。Excelでよく見るあれです。
しつこいでしょうが、処理の流れは以下のような感じです。
KeyValueQuery q1 = new KeyValueQuery("好きな食べ物", "もも");
KeyValueQuery q2 = new KeyValueQuery("出身地", "千葉県");
KeyValueQuery q3 = new KeyValueQuery("趣味", "プログラミング");
AndQuery C = new AndQuery(null);
AndQuery B = new AndQuery(q3, C);
AndQuery A = new AndQuery(q1, q2, B);
C.add(A);
イミュータブルについて(おさらい)
オブジェクトが生成されて以降変更されない(できない)ことをイミュータブルであると言います。たとえば、JavaのStringなんかはイミュータブルです。部分文字列を返すメソッドString#subString()は、元の文字列を変更することなく部分文字列からなる新しい文字列を返します。
コンテナ(コレクション、リスト、配列などなど)でもイミュータブルなものを考えることができますね。コンテナを変更して要素を追加するのではなく、すでにある要素に新しい要素を1つ加えた新しいコンテナを作ると考えれば良いです。(ぜんぶ要素をコピーしなければいけなくて時間かかりそうだな... うまい方法もあるらしい →イミュータブル - Wikipedia)
イミュータブルなCompositeパターン
さて、Compositeパターンに登場するオブジェクトをイミュータブルにすると、循環参照が防げそうです。先ほどのAndQuery
クラスを改良してみました。
例(Queryインターフェース)
AndQuery
を改良して、イミュータブルにしてみました。AndQuery
はnew時に子要素を与えて使います。もし、生成後に子要素を追加したい時にはand(Query...)
を呼び出して、子要素が追加された新しいAndQuery
を作ります。
これだけで先ほどのような循環参照は起きなくなります。というか循環参照するように書けなくなります。試してみましょう:
KeyValueQuery q1 = new KeyValueQuery("好きな食べ物", "もも");
KeyValueQuery q2 = new KeyValueQuery("出身地", "千葉県");
KeyValueQuery q3 = new KeyValueQuery("趣味", "プログラミング");
AndQuery C = new AndQuery(null);
AndQuery B = new AndQuery(q3, C);
AndQuery A = new AndQuery(q1, q2, B);
C = new AndQuery(A); //変数Cの指すオブジェクトが変わるだけで、Cの実体が変更されるわけではない
C.and(A); //Cの実体は変更されず、Aを子要素に持つ新しいオブジェクトができるのみ
なるほど、確かに循環参照するようには書けなくなりました。先ほどはまだ中身がないコンポジットをとりあえず作っておくことができましたが、今度はそれがほとんど意味なくなっています。これで万事解決... なのかな?
問題点
後からコンポーネントを修正したり、コンポジットに追加できない
このパターンだと、あるコンポーネントを修正してもコンポジットは修正前のコンポーネントを参照し続けるため、後からコンポーネントを修正することができなくなってしまいます。コンポジットにコンポーネントを後から追加した場合も同様です。
いや、厳密にはできます。1つずつコンポーネントを修正できないのです。ある時点でコンポーネントを修正しようと思ったら、影響のある(=子要素としてそれを含む)すべてのコンポジットに「ごめ~ん、私変わったので、(同じ階層の)仲間をすべて含んだ新しいコンポジットに生まれ変わって」と頼めばいいのです。できるならの話ですが。
追記: できるっぽい。上のイミュータブル - Wikipediaのページなんかでは、うまいことやってますね。
使いどころあるの?
比較的小規模であったり、階層の全体像が生成時点で見えていて修正が不要なときには使えそうです(といっても、その程度の規模なら「気をつける」だけで循環参照を防げそうにも思えるが)。例に検索クエリーを持ち出したのはそれが理由で、これらの性質を満たしています。ディレクトリ階層のスナップショットとかもいけるかも。
本当に循環参照しない?
今のところこのパターンで循環参照させる方法が思いつかないです。匿名クラスとか使えば書けちゃうかもしれません(でもそれってもはやイミュータブルじゃないのでは)。気づいたらコメントください。
参考資料
- 結城浩「増補改訂版 Java言語で学ぶデザインパターン入門」ソフトバンククリエイティブ