メインコンテンツへ移動

デザインパターン:コンポジットパターン(Composite Pattern)

無料2015-03-07#Design_Pattern#组合模式#Composite Pattern

コンポジットパターンは階層構造を提供し、オブジェクトとオブジェクトの集合の間の違いを無視して、それらを平等に扱うことを可能にします。さらに重要なのは、コンポジットパターンとイテレーターパターンを組み合わせることで、いくつかの素晴らしいことを簡単に実現できることです(階層構造のトラバースのカプセル化や、トラバースを基に拡張されたその他の操作など)。クラスの単一責任原則を犠牲にして、透明性を獲得します。

no_mkd

一.コンポジットパターンとは?

コンポジットパターンは階層構造を提供し、オブジェクトとオブジェクトの集合の間の違いを無視することを可能にします

呼び出し側は手元にあるものが単一のオブジェクトなのかオブジェクトのグループなのかを知りませんが、没关系、コンポジットパターンでは、呼び出し側はこれを知る必要がありません

二.例を挙げる

ファイルシステムを記述すると仮定します。ファイルシステムにはファイルとフォルダーがあり、フォルダーの中にはさらにフォルダーとファイルがあります。。。

そうです、これは階層構造です。メニューのように、メニューにはメニュー項目とサブメニューがあり、サブメニューにはメニュー項目とサブサブメニューがあります。。階層構造はツリー構造でもあります。Node クラスを定義し、一連の子供へのポインターを含めて完全なツリーを構築することを簡単に考え付きます。では、クラス図は以下のようになります:

注意:File はクラス図にリストされている操作のみをサポートし、Folder クラスは継承されたすべての操作をサポートします

クラスの基本的な設計はこの通りで、このようなクラス構造を使ってファイルシステムを記述できます。以下にコード実装を行います:

Directory ベースクラスを定義します:

package CompositePattern;

import java.util.ArrayList;

/**

  • 定義目录类

  • @author ayqy */ public abstract class Directory { String name; String description; ArrayList<Directory> files;

    /**

    • 添加指定文件/文件夹到该目录下
    • @param dir 将要添加的文件/文件夹
    • @return 添加成功/失败 */ public boolean add(Directory dir){ throw new UnsupportedOperationException();//默认抛出操作异常 }

    /**

    • 删除该目录下的指定文件/文件夹
    • @param dir 将要删除的文件/文件夹
    • @return 删除成功/失败 */ public boolean remove(Directory dir){ throw new UnsupportedOperationException();//默认抛出操作异常 }

    /**

    • 清空该目录下所有文件和文件夹
    • @return 清空成功/失败 */ public boolean clear(){ throw new UnsupportedOperationException();//默认抛出操作异常 }

    public ArrayList<Directory> getFiles() { throw new UnsupportedOperationException();//默认抛出操作异常 }

    /**

    • 打印输出 */ public abstract void print();

    public String getName() { return name; }

    public String getDescription() { return description; }

    public String toString(){ return name + description; } }

P.S. ベースクラスで Folder 固有のメソッドの处理方式に注意してください(例外をスローする)。もちろん、より調和的な方法で行うこともでき、それぞれに利点と欠点があります。後述で例外をスローするというこのような乱暴な方式を採用する理由について詳述します

注意、ベースクラスで抽象的な print メソッドを定義しています。print メソッドを呼び出してファイルツリー全体を出力したいと考えています。コンポジットパターンは、この面倒なプロセスを非常に簡単でエレガントな方法で実装することを可能にします

以下に File クラスを実装します:

package CompositePattern;

/**

  • 实现文件类

  • @author ayqy */ public class File extends Directory{

    public File(String name, String desc) { this.name = name; this.description = desc; }

    @Override public void print() { System.out.print(this.toString());//输出文件自身信息 } }

File クラスは非常にシンプルです。ベースクラスで File がサポートしない操作に対してデフォルト実装(例外をスローする)を行っているため、File は非常にスリムになります

次に Folder クラスです:

package CompositePattern;

import java.util.ArrayList;

/**

  • 实现文件夹类

  • @author ayqy */ public class Folder extends Directory{

    public Folder(String name, String desc){ this.name = name; this.description = desc; this.files = new ArrayList<Directory>(); }

    @Override public void print() { //打印该 Folder 自身信息 System.out.print(this.toString() + "("); //打印该目录下所有文件及子文件 for(Directory dir : files){ dir.print(); System.out.print(", "); } //打印文件夹遍历结束标志 System.out.print(")"); }

    @Override public boolean add(Directory dir){ if(files.add(dir)) return true; else return false; }

    @Override public boolean remove(Directory dir){ if(files.remove(dir)) return true; else return false; }

    @Override public boolean clear(){ files.clear();

     return true;
    

    }

    @Override public ArrayList<Directory> getFiles() { return files; } }

Folder クラスはサポートされるすべての操作に対して独自の実装を提供し、print メソッドで少し工夫を凝らしています。非常にシンプルなループで現在ノードのすべての子孫ノードの印刷出力を実現しています(これは何かを連想させませんか?そうです、デコレーターパターンです)。一見すると信じがたく見えますが、这正是使用コンポジットパターンの利点の一つです(再帰に自然な土壌を提供します)

三.効果の例

上記でファイルシステムを記述するために必要なクラスを実装しました。テストして効果を見てみましょう:

テストクラスのコードは以下の通りです:

package CompositePattern;

/**

  • 实现一个测试类

  • @author ayqy / public class Test { public static void main(String[] args){ /构造文件树/ / C a.txt b.txt system

     		sys.dat
     	windows
     		win32
     			settings
     			log.txt
     		win32.config
     */
     Directory dir = new Folder("C", "");
     dir.add(new File("a.txt", ""));
     dir.add(new File("b.txt", ""));
     Directory subDir = new Folder("system", "");
     subDir.add(new File("sys.dat", ""));
     dir.add(subDir);
     Directory subDir2 = new Folder("windows", "");
     Directory subDir3 = new Folder("win32", "");
     subDir3.add(new Folder("settings", ""));
     subDir3.add(new File("log.txt", ""));
     subDir2.add(subDir3);
     subDir2.add(new File("win32.config", ""));
     dir.add(subDir2);
     
     dir.print();//打印输出文件树
    

    } }

実行結果は以下の通りです:

C(a.txt, b.txt, system(sys.dat, ), windows(win32(settings(), log.txt, ), win32.config, ), )

予想通りの結果とほぼ同じですが、美中不足なのは:余分なカンマ区切り文字が存在することです。余分なカンマを削除するには、最後のラウンドをループするときにカンマを出力せず、それ以外のときはカンマを出力するように表示する必要があります

明示的なイテレーターを使用して実装することを簡単に考え付きます(hasNext はまさに最後のラウンドかどうかを判断するために使用不是吗?ArrayList がイテレーターをサポートしていることを忘れないでください)。print メソッドを変更します:

public void print() {
	//打印该 Folder 自身信息
	System.out.print(this.toString() + "(");
	//打印该目录下所有文件及子文件
	Iterator<Directory> iter = getFiles().iterator();
	while(iter.hasNext()){
		Directory dir = iter.next();
		dir.print();
		if(iter.hasNext()){
			System.out.print(",");
		}
	}
	//打印文件夹遍历结束标志
	System.out.print(")");
}

邪魔な余分なカンマを正常に削除できました

四.もう少し変更を加える

関連プログラムが NotePad.exe であるすべてのファイル情報を印刷出力する方法

ではまず、File に新しいプロパティ linkedExe を追加する必要があります。これはそのファイルに関連付けられた実行可能プログラムを表します。フォルダーはこのプロパティをサポートしません(ここではフォルダーが linkedExe プロパティをサポートしないと規定します。フォルダーに関連付けられたプログラムがエクスプローラーなのか何なのかは考慮しません)

新しい要件を実現するために、いくつかの変更を加える必要があります。タイプの一貫性を獲得するために、linkedExe プロパティをベースクラス Directory に追加する必要があります(これは非難されるかもしれませんが、時にはいくつかの利点を犠牲にして他の利点を獲得する必要があります。。)

矩形枠の中の内容は私たちが追加した新しいものです。これらはすべて File がサポートしますが Folder はサポートしないものです。このような変更を行った後、関連プログラムが NotePad.exe であるすべてのファイル情報を印刷出力できます。もちろん、Folder クラスの print メソッドも変更する必要があります:

public void print() {
	//打印该目录下所有关联程序为 NotePad.exe 的文件
	for(Directory dir : files){
		try{
			if("NotePad.exe".equalsIgnoreCase(dir.getLinkedExe())){
				dir.print();
			}
		}catch(UnsupportedOperationException e){
			//吃掉异常,继续遍历(Folder 不支持 getLinkedExe 操作)
		}
	}
}

何か気づきましたか?コンポジットパターンの欠点がますます明らかになってきました

コンポジットパターンはオブジェクトとオブジェクトのグループの間の違いを無視し、それらを平等に扱うことを要求します

そうです、このようにすることで透明性を獲得しました(print メソッドで現在処理しているのが File なのか Folder なのかを知りません)。しかし、私たちは「一視同仁」を追求するために、オブジェクトの集合と単一オブジェクトの差異を隠すために例外処理メカニズムを「濫用」さえしました。これが果たして価値があるかどうかは、具体的な状況によって異なります(私たちは常にいくつかのものを犠牲にして、他のより有用なものを獲得しています。この犠牲が価値があるかどうかはもちろん検討する必要があります)

五.イテレーターとコンポジットパターン

約束したイテレーターはどこですか?どこにも見えませんか?どこにあるのですか?

イテレーターはコンポジットパターンに隠れています。print メソッドの内部でずっとイテレーターを使用していませんか?(暗黙のイテレーターか明示的なイテレーターです。。)

上記の例で使用したイテレーターは内部イテレーターと呼ばれます。つまり、イテレーターはコンポジットパターンの構成クラスに潜んでいるため、発見しにくいです。もちろん、望むなら外部イテレーターを構築することもできます。次のように:

DirectoryIterator では、hasNext と next メソッドを実装するために、現在の位置を記録するためのスタック構造を手動で維持する必要があります(内部イテレーターはシステムスタックによって提供されるサポートです)

実際にはもう一つ問題があります。File クラスは明らかに iterator メソッドをサポートしていませんが、親クラスから継承しています。どのように処理すべきでしょうか?

  • null を返す:呼び出し側は if 文を使用して判断する必要があります
  • 例外をスローする:呼び出し側は例外処理を行う必要があります
  • (推奨される方法)空のイテレーター(NullIterator)を返す:空のイテレーターはどのように実装するか?hasNext が false を返すだけでいいです。。これにより呼び出し側に何の影響もありません

六.まとめ

コンポジットパターンが提供するツリー階層構造により、単一オブジェクトとオブジェクトの集合を平等に扱うことができます(操作上の利便性を獲得)。しかし、このような利点はクラスの単一責任原則を犠牲にして獲得されたものであり、コンポジットパターンは継承を使用して実装されているため、柔軟性に欠けます。

したがって、コンポジットパターンを使用する際は慎重に検討し、このような犠牲が価値があるかどうかを考え、価値がない場合は他のデザインパターンで置き換えられるかどうかを検討する必要があります。。

七.少し余談(例外をスローするかどうかについて)

時には null を返す、false を返す、エラーコードを返すなど、例外をスローしないことを選択できます。これらの方法はより調和的かもしれませんが、例外をスローすることは時には事実に対する最も適切な表現です

例を挙げましょう。File クラスに hasLinkedExe プロパティがあると仮定します。これは関連付けられたアプリケーションが存在するかどうかを表します。Folder は hasLinkedExe プロパティをサポートせず、同時にこのプロパティは親クラスから継承されているため、削除することはできません。

この場合、false を返すか例外をスローするかを選択できます:

  • false を返す:Folder には関連付けられたアプリケーションがないことを表します
  • 例外をスローする:Folder はこの操作をサポートしないことを表します

明らかに、例外をスローする意味こそが私たちが本当に表現したいことです

これだけ話しましたが、例外をスローする方式を使うために多大な努力を払いました。より適切に表現するためだけでしょうか?いいえ、絶対にそう思わないでください。このわずかな意味の差異が深刻な問題を引き起こす可能性があります。例えば:

関連付けられたアプリケーションがないすべてのファイル(つまり「未知のファイル」)を出力すると仮定します。もし当初 false を返す方式を採用してフォルダーがこの操作をサポートしないことを表していた場合、誤った結果が得られるでしょう(すべての未知のファイルとすべてのフォルダーを出力してしまいます。。)

コメント

コメントはまだありません

コメントを書く