学習難易度:★★★☆☆,使用頻度:★★★★☆】
ソフトウェアにおけるツリー構造は至る所に見られます。例えば、オペレーティングシステムのディレクトリ構造、アプリケーションソフトウェアのメニュー、オフィスシステムの会社組織構造などが挙げられます。このようなツリー構造をオブジェクト指向の方法でどのように処理するかが、コンポジットパターンが解決すべき問題です。コンポジットパターンは、巧妙な設計ソリューションを通じて、ユーザーがツリー構造全体またはその一部を一貫して処理できるようにし、ツリー構造の葉ノード(子ノードを含まないノード)とコンテナノード(子ノードを含むノード)を一貫して処理できるようにします。以下では、このツリー構造を処理するためのコンポジットパターンを学習します。
11.1 アンチウイルスソフトウェアのフレームワーク設計
| Sunnyソフトウェア会社はアンチウイルス(AntiVirus)ソフトウェアを開発したいと考えています。このソフトウェアは特定のフォルダ(Folder)をウイルスチェックできるだけでなく、指定されたファイル(File)をウイルスチェックすることもできます。また、このアンチウイルスソフトウェアは、各種ファイルの特性に基づいて、異なるタイプのファイルに対して異なるウイルスチェック方法を提供できます。例えば、画像ファイル(ImageFile)とテキストファイル(TextFile)のウイルスチェック方法は異なります。このアンチウイルスソフトウェアの全体フレームワーク設計方案を提供する必要があります。 |
|---|
介绍Sunny公司开发人员提出的初始解决方案之前,我们先来分析一下操作系统中的文件目录结构,例如在Windows操作系统中,存在如图11-1所示目录结构:
图11-1 Windows目录结构
图11-1可以简化为如图11-2所示树形目录结构:
图11-2 树形目录结构示意图
我們可以看出,在图11-2中包含文件(灰色节点)和文件夹(白色节点)两类不同的元素,其中在文件夹中可以包含文件,还可以继续包含子文件夹,但是在文件中不能再包含子文件或者子文件夹。在此,我们可以称文件夹为容器(Container),而不同类型的各种文件是其成员,也称为叶子(Leaf),一个文件夹也可以作为另一个更大的文件夹的成员。如果我们现在要对某一个文件夹进行操作,如查找文件,那么需要对指定的文件夹进行遍历,如果存在子文件夹则打开其子文件夹继续遍历,如果是文件则判断之后返回查找结果。
Sunny软件公司的开发人员通过分析,决定使用面向对象的方式来实现对文件和文件夹的操作,定义了如下图像文件类ImageFile、文本文件类TextFile和文件夹类Folder:
//为了突出核心框架代码,我们对杀毒过程的实现进行了大量简化
import java.util.*;
//图像文件类
class ImageFile {
private String name;
public ImageFile(String name) {
this.name = name;
}
public void scanVirus() {
//简化代码,模拟杀毒
System.out.println("----图像文件'" + name + "'をウイルスチェック中");
}
}
//文本文件类
class TextFile {
private String name;
public TextFile(String name) {
this.name = name;
}
public void scanVirus() {
//简化代码,模拟杀毒
System.out.println("----テキストファイル'" + name + "'をウイルスチェック中");
}
}
//文件夹类
class Folder {
private String name;
//定义集合folderList,用于存储Folder类型的成员
private ArrayList<Folder> folderList = new ArrayList<Folder>();
//定义集合imageList,用于存储ImageFile类型的成员
private ArrayList<ImageFile> imageList = new ArrayList<ImageFile>();
//定义集合textList,用于存储TextFile类型的成员
private ArrayList<TextFile> textList = new ArrayList<TextFile>();
public Folder(String name) {
this.name = name;
}
//增加新的Folder类型的成员
public void addFolder(Folder f) {
folderList.add(f);
}
//增加新的ImageFile类型的成员
public void addImageFile(ImageFile image) {
imageList.add(image);
}
//增加新的TextFile类型的成员
public void addTextFile(TextFile text) {
textList.add(text);
}
//需提供三个不同的方法removeFolder()、removeImageFile()和removeTextFile()来删除成员,代码省略
//需提供三个不同的方法getChildFolder(int i)、getChildImageFile(int i)和getChildTextFile(int i)来获取成员,代码省略
public void scanVirus() {
System.out.println("****フォルダ'" + name + "'をウイルスチェック中"); //模拟杀毒
//如果是Folder类型的成员,递归调用Folder的scanVirus()方法
for(Object obj : folderList) {
((Folder)obj).scanVirus();
}
//如果是ImageFile类型的成员,调用ImageFile的scanVirus()方法
for(Object obj : imageList) {
((ImageFile)obj).scanVirus();
}
//如果是TextFile类型的成员,调用TextFile的scanVirus()方法
for(Object obj : textList) {
((TextFile)obj).scanVirus();
}
}
}
编写如下客户端测试代码进行测试:
class Client {
public static void main(String args[]) {
Folder folder1,folder2,folder3;
folder1 = new Folder("Sunnyの資料");
folder2 = new Folder("画像ファイル");
folder3 = new Folder("テキストファイル");
ImageFile image1,image2;
image1 = new ImageFile("楊過.jpg");
image2 = new ImageFile("張無忌.gif");
TextFile text1,text2;
text1 = new TextFile("九陰真経.txt");
text2 = new TextFile("葵花宝典.doc");
folder2.addImageFile(image1);
folder2.addImageFile(image2);
folder3.addTextFile(text1);
folder3.addTextFile(text2);
folder1.addFolder(folder2);
folder1.addFolder(folder3);
folder1.scanVirus();
}
}
编译并运行程序,输出结果如下:
| \*\*\*\*フォルダ'Sunnyの資料'をウイルスチェック中 \*\*\*\*フォルダ'画像ファイル'をウイルスチェック中 \----画像ファイル'楊過.jpg'をウイルスチェック中 \----画像ファイル'張無忌.gif'をウイルスチェック中 \*\*\*\*フォルダ'テキストファイル'をウイルスチェック中 \----テキストファイル'九陰真経.txt'をウイルスチェック中 \----テキストファイル'葵花宝典.doc'をウイルスチェック中 |
|---|
Sunny公司开发人员"成功"实现了杀毒软件的框架设计,但通过仔细分析,发现该设计方案存在如下问题:
(1) フォルダクラスFolderの設計と実装が非常に複雑で、複数の集合を定義して異なるタイプのメンバーを保存する必要があり、異なるメンバーに対して追加、削除、取得などのメンバー管理とアクセスメソッドを提供する必要があり、大量の冗長コードが存在し、システムの保守が困難です;
(2) システムが抽象層を提供していないため、クライアントコードはコンテナとして機能するフォルダFolderと葉として機能するImageFileとTextFileを区別して扱う必要があり、それらを一貫して処理できません;
(3) システムの柔軟性と拡張性が低く、新しいタイプの葉とコンテナを追加する必要がある場合、既存のコードを修正する必要があります。例えば、システムに新しいタイプのビデオファイルVideoFileを追加する必要がある場合、Folderクラスのソースコードを修正しないと、フォルダにビデオファイルを追加できません。
面对以上问题,Sunny软件公司的开发人员该如何来解决?这就需要用到本章将要介绍的组合模式,组合模式为处理树形结构提供了一种较为完美的解决方案,它描述了如何将容器和叶子进行递归组合,使得用户在使用时无须对它们进行区分,可以一致地对待容器和叶子。
11.2 组合模式概述
ツリー構造では、コンテナオブジェクト(フォルダなど)のメソッドが呼び出されると、ツリー構造全体を走査し、同じメソッドを持つメンバーオブジェクト(コンテナオブジェクトまたは葉オブジェクト)を見つけて実行します。一つの操作が全体に影響を与えるこのメカニズムは、再帰呼び出しを使用して構造全体を処理します。コンテナオブジェクトと葉オブジェクトの機能的な違いにより、これらのオブジェクトを使用するコードでは、コンテナオブジェクトと葉オブジェクトを区別して扱う必要がありますが、実際には、ほとんどの場合、一貫して処理したいと考えています。これらのオブジェクトの区別はプログラムを非常に複雑にします。コンポジットパターンはこの種の問題を解決するために生まれました。それは葉オブジェクトとコンテナオブジェクトの使用を一貫させることができます。
コンポジットパターンは以下のように定義されます:
| **コンポジットパターン(Composite Pattern):複数のオブジェクトを組み合わせてツリー構造を形成し、「全体-部分」関係を持つ階層構造を表現します。コンポジットパターンは単一オブジェクト(葉オブジェクト)と複合オブジェクト(コンテナオブジェクト)の使用を一貫させます。コンポジットパターンは「全体-部分」(Part-Whole)パターンとも呼ばれ、オブジェクト構造パターンの一種です。** |
|---|
コンポジットパターンでは、抽象コンポーネントクラスComponentが導入されています。これはすべてのコンテナクラスと葉クラスの共通の親クラスであり、クライアントはComponentに対してプログラミングします。コンポジットパターンの構造は図11-3のようになります:
图11-3 コンポジットパターン構造図
図11-3の構造には以下の役割が含まれます:
**● Component(抽象コンポーネント):**インターフェースまたは抽象クラスにすることができ、葉コンポーネントとコンテナコンポーネントオブジェクトのインターフェースを宣言します。この役割では、すべてのサブクラスに共通の動作の宣言と実装を含めることができます。抽象コンポーネントでは、子コンポーネントへのアクセスおよび管理メソッドが定義されています。例えば、子コンポーネントの追加、削除、取得などです。
**● Leaf(葉コンポーネント):**コンポジット構造で葉ノードオブジェクトを表します。葉ノードには子ノードがありません。抽象コンポーネントで定義された動作を実装します。子コンポーネントへのアクセスおよび管理メソッドについては、例外処理などを通じて処理できます。
**● Composite(コンテナコンポーネント):**コンポジット構造でコンテナノードオブジェクトを表します。コンテナノードには子ノードが含まれており、その子ノードは葉ノードまたはコンテナノードにすることができます。子ノードを保存するための集合を提供し、抽象コンポーネントで定義された動作を実装します。これには、子コンポーネントへのアクセスおよび管理メソッドが含まれており、そのビジネスメソッドでは子ノードのビジネスメソッドを再帰的に呼び出すことができます。
**コンポジットパターンの鍵は、抽象コンポーネントクラスを定義することです。それは葉でもコンテナでも表すことができ、クライアントはその抽象コンポーネントクラスに対してプログラミングし、それが葉かコンテナかを知る必要はありません。一貫して処理できます。**同時に、コンテナオブジェクトと抽象コンポーネントクラスの間には集約関連関係が確立されており、コンテナオブジェクトには葉とコンテナの両方を含めることができ、再帰的な組み合わせを実現し、ツリー構造を形成します。
コンポジットパターンを使用しない場合、クライアントコードはコンテナオブジェクトの複雑な内部実装構造に過度に依存し、コンテナオブジェクトの内部実装構造の変更はクライアントコードの頻繁な変更を引き起こし、コードの保守が複雑で、拡張性が低いなどの欠点があります。コンポジットパターンの導入はこれらの問題をある程度解決します。
以下は、コンポジットパターンの各役割の用途と実装を分析するための簡単なサンプルコードです。コンポジットパターンの抽象コンポーネント役割の典型的なコードは以下のようになります:
abstract class Component {
public abstract void add(Component c); //メンバーを追加
public abstract void remove(Component c); //メンバーを削除
public abstract Component getChild(int i); //メンバーを取得
public abstract void operation(); //ビジネスメソッド
}
通常、抽象コンポーネントクラスはインターフェースまたは抽象クラスとして設計され、すべてのサブクラスに共通のメソッドの宣言と実装が抽象コンポーネントクラスに配置されます。クライアントにとって、抽象コンポーネントをプログラミングし、その具体的なサブクラスがコンテナコンポーネントか葉コンポーネントかを気にする必要はありません。
抽象コンポーネントを継承するのが葉コンポーネントの場合、その典型的なコードは以下のようになります:
class Leaf extends Component {
public void add(Component c) {
//例外処理またはエラーメッセージ
}
public void remove(Component c) {
//例外処理またはエラーメッセージ
}
public Component getChild(int i) {
//例外処理またはエラーメッセージ
return null;
}
public void operation() {
//葉コンポーネントの具体的なビジネスメソッドの実装
}
}
抽象コンポーネントクラスのサブクラスとして、葉コンポーネントでは抽象コンポーネントクラスで宣言されたすべてのメソッドを実装する必要があります。これにはビジネスメソッドおよび子コンポーネントの管理とアクセスメソッドが含まれますが、葉コンポーネントはさらに子コンポーネントを含めることができないため、**葉コンポーネントで子コンポーネントの管理とアクセスメソッドを実装する際には、例外処理またはエラーメッセージを提供する必要があります。**もちろん、これは葉コンポーネントの実装に面倒を引き起こします。
抽象コンポーネントを継承するのがコンテナコンポーネントの場合、その典型的なコードは以下のようになります:
class Composite extends Component {
private ArrayList<Component> list = new ArrayList<Component>();
public void add(Component c) {
list.add(c);
}
public void remove(Component c) {
list.remove(c);
}
public Component getChild(int i) {
return (Component)list.get(i);
}
public void operation() {
//コンテナコンポーネントの具体的なビジネスメソッドの実装
//メンバーコンポーネントのビジネスメソッドを再帰的に呼び出す
for(Object obj:list) {
((Component)obj).operation();
}
}
}
コンテナコンポーネントでは、抽象コンポーネントで宣言されたすべてのメソッドを実装します。これにはビジネスメソッドだけでなく、メンバー子コンポーネントへのアクセスと管理のためのメソッド(add()、remove()、getChild()など)も含まれます。具体的なビジネスメソッドを実装する際には、コンテナコンポーネントがコンテナの役割を果たし、メンバーコンポーネントを含んでいるため、メンバーコンポーネントのビジネスメソッドを呼び出します。**コンポジットパターン構造では、コンテナコンポーネントにまだコンテナコンポーネントを含めることができるため、コンテナコンポーネントを処理する際には再帰アルゴリズムを使用する必要があります。**つまり、コンテナコンポーネントのoperation()メソッドでメンバーコンポーネントのoperation()メソッドを再帰的に呼び出します。
| | | **思考** コンポジットパターン構造図において、集約関連関係がCompositeからComponentではなく、CompositeからLeafである場合、図11-4のように、どのような結果が生じるでしょうか? **图11-4 コンポジットパターン思考題構造図** 11.3 完整解决方案 ----------- システムの柔軟性と拡張性を向上させるため、クライアントがファイルとフォルダを一貫して扱えるように、Sunny公司开发人员使用コンポジットパターン来进行杀毒软件的框架设计,其基本结构如图11-5所示: **图11-5 アンチウイルスソフトウェアフレームワーク設計構造図** 図11-5では、AbstractFileが抽象コンポーネントクラス、Folderがコンテナコンポーネントクラス、ImageFile、TextFile、VideoFileが葉コンポーネントクラスとして機能します。完全なコードは以下の通りです: | |---|---| |
|---|
import java.util.*;
//抽象ファイルクラス:抽象コンポーネント
abstract class AbstractFile {
public abstract void add(AbstractFile file);
public abstract void remove(AbstractFile file);
public abstract AbstractFile getChild(int i);
public abstract void scanVirus();
}
//画像ファイルクラス:葉コンポーネント
class ImageFile extends AbstractFile {
private String name;
public ImageFile(String name) {
this.name = name;
}
public void add(AbstractFile file) {
System.out.println("申し訳ありませんが、このメソッドはサポートされていません!");
}
public void remove(AbstractFile file) {
System.out.println("申し訳ありませんが、このメソッドはサポートされていません!");
}
public AbstractFile getChild(int i) {
System.out.println("申し訳ありませんが、このメソッドはサポートされていません!");
return null;
}
public void scanVirus() {
//模拟杀毒
System.out.println("----画像ファイル'" + name + "'をウイルスチェック中");
}
}
//テキストファイルクラス:葉コンポーネント
class TextFile extends AbstractFile {
private String name;
public TextFile(String name) {
this.name = name;
}
public void add(AbstractFile file) {
System.out.println("申し訳ありませんが、このメソッドはサポートされていません!");
}
public void remove(AbstractFile file) {
System.out.println("申し訳ありませんが、このメソッドはサポートされていません!");
}
public AbstractFile getChild(int i) {
System.out.println("申し訳ありませんが、このメソッドはサポートされていません!");
return null;
}
public void scanVirus() {
//模拟杀毒
System.out.println("----テキストファイル'" + name + "'をウイルスチェック中");
}
}
//ビデオファイルクラス:葉コンポーネント
class VideoFile extends AbstractFile {
private String name;
public VideoFile(String name) {
this.name = name;
}
public void add(AbstractFile file) {
System.out.println("申し訳ありませんが、このメソッドはサポートされていません!");
}
public void remove(AbstractFile file) {
System.out.println("申し訳ありませんが、このメソッドはサポートされていません!");
}
public AbstractFile getChild(int i) {
System.out.println("申し訳ありませんが、このメソッドはサポートされていません!");
return null;
}
public void scanVirus() {
//模拟杀毒
System.out.println("----ビデオファイル'" + name + "'をウイルスチェック中");
}
}
//フォルダクラス:コンテナコンポーネント
class Folder extends AbstractFile {
//定义集合fileList,用于存储AbstractFile类型的成员
private ArrayList<AbstractFile> fileList=new ArrayList<AbstractFile>();
private String name;
public Folder(String name) {
this.name = name;
}
public void add(AbstractFile file) {
fileList.add(file);
}
public void remove(AbstractFile file) {
fileList.remove(file);
}
public AbstractFile getChild(int i) {
return (AbstractFile)fileList.get(i);
}
public void scanVirus() {
System.out.println("****フォルダ'" + name + "'をウイルスチェック中"); //模拟杀毒
//递归调用成员构件的scanVirus()方法
for(Object obj : fileList) {
((AbstractFile)obj).scanVirus();
}
}
}
编写如下客户端测试代码:
class Client {
public static void main(String args[]) {
//抽象コンポーネントに対してプログラミング
AbstractFile file1,file2,file3,file4,file5,folder1,folder2,folder3,folder4;
folder1 = new Folder("Sunnyの資料");
folder2 = new Folder("画像ファイル");
folder3 = new Folder("テキストファイル");
folder4 = new Folder("ビデオファイル");
file1 = new ImageFile("楊過.jpg");
file2 = new ImageFile("張無忌.gif");
file3 = new TextFile("九陰真経.txt");
file4 = new TextFile("葵花宝典.doc");
file5 = new VideoFile("笑傲江湖.rmvb");
folder2.add(file1);
folder2.add(file2);
folder3.add(file3);
folder3.add(file4);
folder4.add(file5);
folder1.add(folder2);
folder1.add(folder3);
folder1.add(folder4);
//「Sunnyの資料」ノードからウイルスチェック操作を開始
folder1.scanVirus();
}
}
编译并运行程序,输出结果如下:
| \*\*\*\*フォルダ'Sunnyの資料'をウイルスチェック中 \*\*\*\*フォルダ'画像ファイル'をウイルスチェック中 \----画像ファイル'楊過.jpg'をウイルスチェック中 \----画像ファイル'張無忌.gif'をウイルスチェック中 \*\*\*\*フォルダ'テキストファイル'をウイルスチェック中 \----テキストファイル'九陰真経.txt'をウイルスチェック中 \----テキストファイル'葵花宝典.doc'をウイルスチェック中 \*\*\*\*フォルダ'ビデオファイル'をウイルスチェック中 \----ビデオファイル'笑傲江湖.rmvb'をウイルスチェック中 |
|---|
本インスタンスではコンポジットパターンを使用しているため、抽象コンポーネントクラスでadd()メソッドやremove()メソッドなどのすべてのメソッドが宣言されており、ImageFileなどの葉コンポーネントクラスでこれらのメソッドを実装する際には、対応する例外処理やエラーメッセージを提供する必要があります。コンテナコンポーネントクラスFolderのscanVirus()メソッドでは、メンバーオブジェクトのscanVirus()メソッドを再帰的に呼び出し、ツリー構造全体を走査します。
操作ノードを変更する必要がある場合、例えばフォルダ「テキストファイル」のみをウイルスチェックする場合、クライアントコードは1行だけ変更する必要があります。コード:
| folder1.scanVirus(); |
|---|
を以下のように変更します:
| folder3.scanVirus(); |
|---|
出力結果は以下の通りです:
| \*\*\*\*フォルダ'テキストファイル'をウイルスチェック中 \----テキストファイル'九陰真経.txt'をウイルスチェック中 \----テキストファイル'葵花宝典.doc'をウイルスチェック中 |
|---|
具体的な実装では、ユーザーが操作するルートノードを選択できるグラフィカルインターフェースを作成し、ソースコードを変更する必要はありません。これは「オープン・クローズド原則」に準拠しており、クライアントはノードの階層構造を気にする必要がなく、選択したノードを一貫して処理できます。これにより、システムの柔軟性が向上します。
11.4 透明コンポジットパターンと安全コンポジットパターン
コンポジットパターンを導入することで、Sunny公司设计的杀毒软件は良好な拡張性を持ち、新しいファイルタイプを追加する際に、既存のクラスライブラリコードを修正する必要はありません。新しいファイルクラスをAbstractFileクラスのサブクラスとして追加するだけで済みます。しかし、AbstractFileでadd()、remove()などのメンバーを管理およびアクセスするためのメソッドが大量に宣言されているため、新しいファイルクラスでこれらのメソッドを実装し、対応するエラーメッセージや例外処理を提供する必要があります。コードを簡素化するために、以下の2つのソリューションがあります:
**ソリューション1:**葉コンポーネントのadd()、remove()などのメソッドの実装コードをAbstractFileクラスに移動し、AbstractFileが統一されたデフォルト実装を提供します。コードは以下の通りです:
//デフォルト実装を提供する抽象コンポーネントクラス
abstract class AbstractFile {
public void add(AbstractFile file) {
System.out.println("申し訳ありませんが、このメソッドはサポートされていません!");
}
public void remove(AbstractFile file) {
System.out.println("申し訳ありませんが、このメソッドはサポートされていません!");
}
public AbstractFile getChild(int i) {
System.out.println("申し訳ありませんが、このメソッドはサポートされていません!");
return null;
}
public abstract void scanVirus();
}
クライアントコードが抽象クラスAbstractFileに対してプログラミングしている場合、ファイルオブジェクトのこれらのメソッドを呼び出すとエラーメッセージが表示されます。エラーメッセージを表示したくない場合は、クライアントで抽象層を使用せず、具体的な葉コンポーネント自体を使用してファイルオブジェクトを定義し、クライアントコードの一部は以下のようになります:
class Client {
public static void main(String args[]) {
//葉コンポーネントを透明に処理できない
ImageFile file1,file2;
TextFile file3,file4;
VideoFile file5;
AbstractFile folder1,folder2,folder3,folder4;
//他のコードは省略
}
}
これにより、不透明な使用方法が生じます。つまり、クライアントはすべての抽象コンポーネントクラスに対してプログラミングできず、具体的な葉コンポーネントタイプを使用して葉オブジェクトを定義する必要があります。
**ソリューション2:**別の解決方法は、抽象コンポーネントAbstractFileでメンバーをアクセスおよび管理するためのメソッドを宣言しないことです。コードは以下の通りです:
abstract class AbstractFile {
public abstract void scanVirus();
}
この場合、AbstractFileでadd()、remove()などのメンバーをアクセスおよび管理するメソッドが宣言されていないため、葉コンポーネントのサブクラスは実装を提供する必要がありません。また、クライアントが葉コンポーネントオブジェクトをどのように定義しても、これらのメソッドを呼び出すことはできず、エラー処理や例外処理は不要です。コンテナコンポーネントは必要に応じてアクセスおよび管理メンバーのメソッドを追加しますが、ここにも問題があります:クライアントはコンテナクラス自体を使用してコンテナコンポーネントオブジェクトを宣言する必要があります。そうしないと、追加されたadd()、remove()などのメソッドにアクセスできません。クライアントが葉とコンテナを一貫して扱う場合、コンテナコンポーネントの追加はクライアントに不可視になり、クライアントコードはコンテナコンポーネントに対して抽象コンポーネントを使用できなくなります。クライアントコードの一部は以下のようになります:
class Client {
public static void main(String args[]) {
AbstractFile file1,file2,file3,file4,file5;
Folder folder1,folder2,folder3,folder4; //コンテナコンポーネントを透明に処理できない
//他のコードは省略
}
}
コンポジットパターンを使用する際、抽象コンポーネントクラスの定義形式に基づいて、コンポジットパターンを透明コンポジットパターンと安全コンポジットパターンの2つの形式に分けることができます:
(1) 透明コンポジットパターン
透明コンポジットパターンでは、抽象コンポーネントComponentでadd()、remove()、getChild()などのメンバーを管理するためのすべてのメソッドが宣言されています。これにより、すべてのコンポーネントクラスが同じインターフェースを持つことが保証されます。クライアントから見ると、葉オブジェクトとコンテナオブジェクトが提供するメソッドは一貫しています。クライアントはすべてのオブジェクトを同じように扱うことができます。透明コンポジットパターンはコンポジットパターンの標準形式でもあります。上記のソリューション1ではクライアントに不透明な実装方法があるかもしれませんが、抽象コンポーネントにadd()、remove()などのメソッドが含まれているため、透明コンポジットパターンです。透明コンポジットパターンの完全な構造は図11-6のようになります:
图11-6 透明コンポジットパターン構造図
透明コンポジットパターンの欠点は安全性が低いことです。葉オブジェクトとコンテナオブジェクトは本質的に異なります。葉オブジェクトは次のレベルのオブジェクトを持つことができず、メンバーオブジェクトを含めることができないため、add()、remove()、getChild()などのメソッドを提供する意味はありません。これはコンパイル段階ではエラーになりませんが、実行段階でこれらのメソッドを呼び出すとエラーが発生する可能性があります(対応するエラーハンドリングコードがない場合)。
(2) 安全コンポジットパターン
安全コンポジットパターンでは、抽象コンポーネントComponentでメンバーを管理するためのメソッドは宣言されていません。代わりに、Compositeクラスでこれらのメソッドが宣言および実装されています。この方法は安全です。なぜなら、葉オブジェクトにこれらのメンバーを管理するメソッドを提供しないため、クライアントがこれらのメソッドを呼び出すことはできないからです。これはソリューション2が採用している実装方法です。安全コンポジットパターンの構造は図11-7のようになります:
图11-7 安全コンポジットパターン構造図
安全コンポジットパターンの欠点は透明性が低いことです。葉コンポーネントとコンテナコンポーネントは異なるメソッドを持っており、コンテナコンポーネントのメンバーを管理するメソッドは抽象コンポーネントクラスで定義されていないため、クライアントは完全に抽象クラスに対してプログラミングできず、葉コンポーネントとコンテナコンポーネントを区別して扱う必要があります。実際のアプリケーションでは、安全コンポジットパターンの使用頻度も非常に高く、Java AWTで使用されているコンポジットパターンは安全コンポジットパターンです。
11.5 公司组织结构
コンポジットパターンの学習と使用中に、Sunny软件公司开发人员はツリー構造が実際には至る所に存在することに気づきました。例えば、Sunny公司の組織構造は「標準的なツリー」です。図11-8のように:
图11-8 Sunny公司組織構造図
Sunny软件公司の内部オフィスシステムSunny OAシステムには、会社組織構造に対応するツリーメニューがあります。行政担当者は各レベルの単位に通知を発行できます。これらの単位は、本社の部門、支社、または支社の部門である可能性があります。ユーザーはルートノードを選択するだけで通知の発行操作を実現できます。具体的な実装の詳細を気にする必要はありません。これこそがコンポジットパターンの「特長」ではありませんか?そこで、Sunny公司开发人员は図11-9のような構造図を描きました:
图11-9 Sunny公司組織構造コンポジットパターン図
図11-9では、「単位」が抽象コンポーネントの役割を果たし、「会社」がコンテナコンポーネントの役割を果たし、「研究開発部」、「財務部」、「人事部」が葉コンポーネントの役割を果たします。
| | | **思考** 図11-9の「会社」クラスをどのようにコーディングしますか? | |---|---| |
|---|
11.6 组合模式总结
コンポジットパターンはオブジェクト指向の思想を使用してツリー構造の構築と処理を実現し、ツリー構造のコンテナオブジェクトと葉オブジェクトを再帰的に組み合わせる方法を説明します。実装が簡単で、柔軟性が高いです。ソフトウェア開発には大量のツリー構造が存在するため、コンポジットパターンは使用頻度の高い構造デザインパターンです。Java SEのAWTとSwingパッケージの設計はコンポジットパターンに基づいており、これらのインターフェースパッケージでは、ユーザーに大量のコンテナコンポーネント(例:Container)とメンバーコンポーネント(例:Checkbox、Button、TextComponentなど)を提供しています。その構造は図11-10のようになります:
图11-10 AWTコンポジットパターン構造図
図11-10では、Componentクラスが抽象コンポーネントで、Checkbox、Button、TextComponentが葉コンポーネントで、Containerがコンテナコンポーネントです。AWTには他にも多くの葉コンポーネントが含まれていますが、ページの制限により図にすべてリストアップしていません。コンテナコンポーネントには葉コンポーネントを含めることができ、さらにコンテナコンポーネントを含めることもできます。これらの葉コンポーネントとコンテナコンポーネントが一緒になって複雑なGUIインターフェースを形成します。
これ以外に、XML解析、組織構造ツリー処理、ファイルシステム設計などの分野でも、コンポジットパターンは広く応用されています。
1. 主要な利点
コンポジットパターンの主要な利点は以下の通りです:
(1) コンポジットパターンは階層的に複雑なオブジェクトを明確に定義し、オブジェクトの全体または一部の階層を表現します。クライアントが階層の違いを無視し、全体の階層構造を制御しやすくします。
(2) クライアントは一貫してコンポジット構造またはその単一オブジェクトを使用でき、処理しているのが単一オブジェクトか全体のコンポジット構造かを気にする必要がありません。クライアントコードを簡素化します。
(3) コンポジットパターンでは、新しいコンテナコンポーネントと葉コンポーネントを追加するのが簡単で、既存のクラスライブラリを修正する必要がありません。これは「オープン・クローズド原則」に準拠しています。
(4) コンポジットパターンはツリー構造のオブジェクト指向実装に柔軟なソリューションを提供します。葉オブジェクトとコンテナオブジェクトの再帰的な組み合わせを通じて、複雑なツリー構造を形成できますが、ツリー構造の制御は非常に簡単です。
2. 主要な欠点
コンポジットパターンの主要な欠点は以下の通りです:
新しいコンポーネントを追加する際に、コンテナ内のコンポーネントタイプを制限するのが難しいです。特定のタイプのオブジェクトしか含めることができないコンテナを希望する場合(例:特定のフォルダにテキストファイルのみを含めることができる場合)、コンポジットパターンを使用すると、それらが同じ抽象レイヤーから来ているため、タイプシステムに依存してこれらの制約を適用できません。この場合、実行時にタイプチェックを実行する必要があり、この実装プロセスは比較的複雑です。
3. 適用シナリオ
以下の場合にコンポジットパターンを使用することを検討できます:
(1) 全体と部分の階層構造があり、全体と部分の違いを無視する方法を通じて、クライアントが一貫して扱えるようにしたい場合。
(2) オブジェクト指向言語で開発されたシステムでツリー構造を処理する必要がある場合。
(3) システムで葉オブジェクトとコンテナオブジェクトを分離でき、そのタイプが固定されておらず、新しいタイプを追加する必要がある場合。
| | | **練習** Sunny软件公司はインターフェースコントロールライブラリを開発したいと考えています。インターフェースコントロールは2つの主要なカテゴリに分かれています。一つは単位コントロール(例:ボタン、テキストボックスなど)で、もう一つはコンテナコントロール(例:ウィンドウ、中間パネルなど)です。コンポジットパターンを使用してこのインターフェースコントロールライブラリを設計してください。 | |---|---| |
|---|