Compositeパターン


 ★☆☆

Compositeパターン

再帰的な構造の取り扱いを容易にする

Composite とは、英語で「複合物」を意味する言葉です。 Composite パターンは、「中身と容器を同一視する」ことで、再帰的な構造の取り扱いを容易にするものです。具体的に言いますと、 「中身」とは、個々のオブジェクトであり、「容器」とは、それらオブジェクトが合成したものです。また、「同一視する」とは、すべてのオブジェクトに共通するインタフェースを持つことを意味します。

アプリケーションで扱うデータをモデル化してみると、オブジェクト同士の関係を単純な1対1の関係だけで管理できることはほとんどありません。多くのオブジェクトは、他のいくつかのオブジェクトへの関わりを持つことで構造を作っています。この構造が単純なうちはよいのですが、また別のオブジェクトへと次々に関わりを持っていくことで、構造はどんどん複雑になっていき、直感的に実装したり使ったりすることが難しくなります。

そんなときは Composite パターンが威力を発揮します。Composite パターンは、要素であるオブジェクトと、複数の要素からなる複合オブジェクトを区別なく扱えるという特徴を持ちます。この特徴を利用することで、構造を再帰的に組み立て、クライアントからの見た目をシンプルに保つことができます。


ただし、再帰構造であっても、容器と中身を同一視しない場合は、Compositeパターンではありません。再帰構造自体は、オブジェクト指向でなくても用いられ、あるメソッドが自分自身を呼び出すという形で使われます。このとき容器か中身かを判断する条件文が使われているのですが、Compositeパターンを使うことで、その条件文を排除し、ポリモーフィズムによって、容器と中身による実行内容の違いを実現します。

こうした例として、ファイルシステムがあります。 ファイルは中身ですが、フォルダは他のフォルダの中身でもあり、自分の持つファイルの容器でもあります。

また、家族関係などもこうした例です。親子三代の家族です。一代目の夫婦は、二代目の親です。しかし、二代目の夫婦は、一代目のおじいさん、おばあさんから見れば子供ですが、三代目の孫の親でもあります。


アプリケーションを作っていると、コンポジットパターンが適用できそうな再帰的構造を持つデータに出会う機会は非常に多いと言えます。身近なところでは上に挙げたファイルとフォルダの関係がそうですし、XMLで表現されるような構造を持つデータは、コンポジットパターンが適用できる好例です。

膨大な規模のデータ構造を目の当たりにすると、とかく複雑に考えがちですが、構成要素をよく観察し、コンポジットパターンが適用できないか一度考えてみましょう。



例題

ファイル構成
dir1
├ file1
├ dir2
│ ├ file2
│ └ file3
└ file4

ファイルシステムは、ディレクトリとファイルの階層構造でできています。指定されたディレクトリの中のファイルとそのサイズを表示するクラスを作成しなさい。もちろん、さらに下層のディレクトリの中のファイルも表示します。


実行結果
dir1\file1 (10MB)
dir1\dir2\file2 (20MB)
dir1\dir2\file3 (30MB)
dir1\file4 (40MB)









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

Directory.java
public class Directory { private List<Object> list = null; private String name = null; public Directory(String name) { this.name = name; list = new ArrayList<Object>(); } public void add(File file) { list.add(file); } public void add(Directory dir) { list.add(dir); } public void dir(String path) { path += name + "\\"; Iterator<Object> it = list.iterator(); while (it.hasNext()) { Object obj = it.next(); if (obj instanceof File) { ((File) obj).dir(path); } else if (obj instanceof Directory) { ((Directory) obj).dir(path); } } } }
File.java
public class File { private String name = null; private int size = 0; public File(String name, int size) { this.name = name; this.size = size; } public void dir(String path) { System.out.println(path + name + " (" + size + "MB)"); } }
Main.java
public class Main { public static void main(String[] args) { File file1 = new File("file1", 10); File file2 = new File("file2", 20); File file3 = new File("file3", 30); File file4 = new File("file4", 40); Directory dir1 = new Directory("dir1"); dir1.add(file1); Directory dir2 = new Directory("dir2"); dir2.add(file2); dir2.add(file3); dir1.add(dir2); dir1.add(file4); dir1.dir(""); } }

Compositeパターンを使用した例

IEntry.java
public interface IEntry { void dir(String path); }
Directory.java
public class Directory implements IEntry { private List<IEntry> list = null; private String name = null; public Directory(String name) { this.name = name; list = new ArrayList<IEntry>(); } public void add(IEntry entry) { list.add(entry); } public void dir(String path) { path += name + "\\"; Iterator<IEntry> it = list.iterator(); while (it.hasNext()) { IEntry entry = it.next(); entry.dir(path); } } }
File.java
public class File implements IEntry { private String name = null; private int size = 0; public File(String name, int size) { this.name = name; this.size = size; } public void dir(String path) { System.out.println(path + name + " (" + size + "MB)"); } }
Main.java
public class Main { public static void main(String[] args) { File file1 = new File("file1", 10); File file2 = new File("file2", 20); File file3 = new File("file3", 30); File file4 = new File("file4", 40); Directory dir1 = new Directory("dir1"); dir1.add(file1); Directory dir2 = new Directory("dir2"); dir2.add(file2); dir2.add(file3); dir1.add(dir2); dir1.add(file4); dir1.dir(""); } }

ディレクトリとファイルを考えます。

ファイルを表す File クラスは、インスタンス変数 name と size を持ちます。また、dir メソッドが呼ばれると、「ファイル名(サイズ)」と表示するものとします。 ディレクトリを表す、Directory クラスは、List オブジェクトとして、配下のディレクトリとファイルのオブジェクトを管理し、dir メソッドが呼ばれた場合には、Fileをすべて表示します。

これでも問題なく動作します。しかし、dir メソッドの中で、ファイルなのかディレクトリなのかを判断していますので、もしここで、「ディレクトリには、ディレクトリとファイルだけでなくショートカットも入るようにしたい」という要求が出てきたらどうでしょう。Directory クラスは、add(Shortcut link) メソッドを追加したり、dir メソッドの中で、 Shortcut に対応できるようにしなければならなくなります。

Composite パターンでは、容器の中身と入れ物を同一視します。同一視するために、容器と中身が共通のインタフェースを実装するようにします。ここでは、File と Directory が共通のインタフェース IEntry を実装しています。

IEntry インタフェースでは、dir メソッドのみを定義しています。これを実装する形で、Directory クラス、File クラスを作成すると、例のようになります。

Directory クラスは、内部にディレクトリやファイルを持てますので、add メソッドが追加されています。また、dir メソッドは、パスを表示します。そして、インスタンスが Directory ならば、さらにそのインスタンスの dir メソッドを呼び出し、そのパスを表示します。インスタンスが File ならば、そのインスタンスの dir メソッドを呼び出し、ファイル名とサイズを表示します。共通のインタフェースである IEntry から呼び出すことによってポリモーフィズムが利用されています。

File クラスは、dir メソッドがファイル名とサイズを表示します。

このように、Directory クラス、File クラスを共に IEntry インタフェースを実装するクラスとすることで、 Directory クラスの dir メソッド内では、実態が File クラスのインスタンスであるのか、Directory クラスのインスタンスであるのかを気にせず、どちらも IEntry インスタンスとして扱うことができるようになっています。まさに、容器と中身を同一視している状態です。

もしここで、 Shortcut クラスを追加する必要が生じたとしも、 柔軟に対応できます。Directory クラスのソースコードを修正する必要もありません。ただ、IEntry インタフェースを実装するように、 Shortcut クラスを実装すればよいのです。



この例では、IEntry インタフェースを実装した Directoryクラスで addメソッドを定義しています。実際、addメソッドを使えるのが Directoryクラスだけだからです。しかし、 addメソッドの置き方、実装の仕方にはいろいろな場合があります。

抽象クラス Entryに実装し、例外を発生させる。
addメソッドを Entryクラスに実装し、例外を発生させます。addメソッドを実際に使える Directoryクラスでは、 Entryクラスのaddメソッドをオーバーライドして、意味のある実装に置き換えます。Fileクラスでは、 Entryクラスのaddメソッドを継承しているのでaddは可能ですが、例外が投げられます。
抽象クラス Entryに実装し、何も行わない。
addメソッドを Entryクラスに実装するが、何も行わないようにすることもできます。そうすると、Fileクラスからのaddは例外も発生しないかわりに、何も起きないということになります。
抽象クラス Entryで宣言はするが、実装はしない。
addメソッドを Entryクラスでは抽象メソッドとしておき、サブクラスでは、必要ならば意味のある実装を定義し、必要でないならば例外を発生させたり、何もしないという実装にします。ただし、本来そのサブクラスでは不要なメソッドをも記述しなければならなくなります。
Directoryクラスにだけ記述する。
この例題がそうです。addメソッドを 抽象クラス Entry(この例では IEntry インタフェース)には入れずに、本当に必要なDirectoryクラスにだけ入れるというやり方です。ただし、このやり方だと、Directoryのインスタンスが Entry型の変数に入っているときは、 Directory型にキャストしないとaddメソッドが使えないことになります。