LoginSignup
0
0

More than 5 years have passed since last update.

イミュータブルにすることでCompositeパターンの循環参照を防いだはずだった

Last updated at Posted at 2017-08-22

はじめに & 概要

 デザインパターンの1つ、Compositeパターンに登場するオブジェクトをイミュータブルにしたら、循環参照を防ぐことができました。でも後で気づいたら、(多くの場合)全然使い物になりませんでした。

前知識

Compositeパターンについて(おさらい)

 Compositeパターンは、ディレクトリ階層のように入れ子状の構造を表すために使われるデザインパターンです。GoFの23のデザインパターンでも取り上げられ、「容器と中身を同一視」することが特徴です。
composite.png
 上のクラス図では、Compositeクラスがフォルダーなどの入れ物を、Leafクラスがファイルなどのアイテムを示していて、Component抽象クラスを使うことでこれらを同一視しています。CompositeComponentを要素として持ち、これによりLeafCompositeも要素にすることが可能です。Compositeに要素を追加するにはadd(Component)を使います。

Compositeパターンで発生する循環参照について

 Compositeクラスは、生成後もadd(Component)を呼び出すことができるため、循環参照が起きる可能性があります。
 データベースの検索クエリーのようなものを考えて具体例で説明します。Queryインターフェースはjudge(Object)抽象メソッドを持ち、オブジェクトがクエリーに該当するかどうかを判定します。KeyValueQueryクラスは値とキーの組み合わせで表現されるクエリーです(おそらく、渡されたオブジェクトからkeyに対応する値を取り出し、valueと照合するのでしょう)。AndQueryクラスは複数のQueryを子要素として持ち、すべての子要素であるクエリーに該当するかどうかを判定します。

クラス図(Query_ ミュータブル).png

 循環参照が発生する状況を示すため、さらに具体化したオブジェクト図を描いてみました。AのAndQueryが「もも」と「千葉県」のKeyValueQueryとBのAndQueryを子要素として持っています。Bもまた子要素を持ち、CのAndQueryはさらにAを子要素として持っています。
オブジェクト図(Query_ イミュータブル).png
 おわかりでしょうが、あるオブジェクトがAのAndQueryに該当するかどうかを調べるため、A.judge()を呼び出すと、BやCのjudge()も連鎖的に呼び出され、またA.judge()が呼び出されてしまいます。Excelでよく見るあれです。
 しつこいでしょうが、処理の流れは以下のような感じです。

Main.java#main
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インターフェース)

クラス図(Query_ イミュータブル).png
 AndQueryを改良して、イミュータブルにしてみました。AndQueryはnew時に子要素を与えて使います。もし、生成後に子要素を追加したい時にはand(Query...)を呼び出して、子要素が追加された新しいAndQueryを作ります。
 これだけで先ほどのような循環参照は起きなくなります。というか循環参照するように書けなくなります。試してみましょう:

Main2.java#main
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つずつコンポーネントを修正できないのです。ある時点でコンポーネントを修正しようと思ったら、影響のある(=子要素としてそれを含む)すべてのコンポジットに「ごめ~ん、私変わったので、(同じ階層の)仲間をすべて含んだ新しいコンポジットに生まれ変わって:two_hearts:」と頼めばいいのです。できるならの話ですが。

追記: できるっぽい。上のイミュータブル - Wikipediaのページなんかでは、うまいことやってますね。

使いどころあるの?

 比較的小規模であったり、階層の全体像が生成時点で見えていて修正が不要なときには使えそうです(といっても、その程度の規模なら「気をつける」だけで循環参照を防げそうにも思えるが)。例に検索クエリーを持ち出したのはそれが理由で、これらの性質を満たしています。ディレクトリ階層のスナップショットとかもいけるかも。

本当に循環参照しない?

 今のところこのパターンで循環参照させる方法が思いつかないです。匿名クラスとか使えば書けちゃうかもしれません(でもそれってもはやイミュータブルじゃないのでは)。気づいたらコメントください。

参考資料

  • 結城浩「増補改訂版 Java言語で学ぶデザインパターン入門」ソフトバンククリエイティブ
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0