blog

デザインパターン学習ノート:組み合わせパターン

ファイルディレクトリのようなツリー構造では、フォルダは複数のフォルダやファイルを含むことができますが、ファイルはサブファイルやサブフォルダを含むことができず、フォルダをコンテナ、ファイルをリーフと呼ぶ...

May 26, 2020 · 10 min. read
シェア

概要

はじめに

ファイルディレクトリのようなツリー構造では、フォルダは複数のフォルダやファイルを含むことができますが、ファイルはサブファイルやサブフォルダを含むことができません。

ツリー構造では、コンテナ・オブジェクトのメソッドが呼び出されると、そのメソッドを含むメンバ・オブジェクトを探してフォルダ全体を走査し、そのメソッドを呼び出して実行します。コンテナ・オブジェクトとリーフ・オブジェクトには機能的な違いがあるため、これらのオブジェクトを使用するコードでは、コンテナ・オブジェクトとリーフ・オブジェクトを異なるように扱う必要がありますが、ほとんどの場合、一貫して扱う必要があります。

コンビナトリアルパターンは、コンテナオブジェクトだけでなくリーフオブジェクトの使用にも一貫性を持たせることで、このような問題を解決するために考案されました。

定義

組み合わせパターン:複数のオブジェクトを組み合わせてツリー構造を形成し、「全体-部分」の関係を持つ階層構造を表します。組み合わせパターンは、個々のオブジェクトの使用とオブジェクトの組み合わせに一貫性があります。

組み合わせパターンは「部分-全体」パターンとも呼ばれ、オブジェクト構造型パターンの一種です。

構造図

役割

  • コンポーネント:リーフコンポーネントオブジェクトとコンテナコンポーネントオブジェクトのインタフェースを宣言するインタフェースまたは抽象クラスであり、この役割において、すべてのサブクラスに共通する振る舞いの宣言と実装を含むことができます。抽象コンポーネントは、そのサブコンポーネントにアクセスし、管理するためのメソッドを定義します。
  • リーフ:リーフノードオブジェクトを表します。リーフノードには子がなく、抽象アーティファクトで定義された振る舞いを実装します。
  • Composite: コンテナノードオブジェクトを表し、コンテナノードはリーフノードまたはコンテナノードとなる子ノードを含み、子ノードを格納するためのコレクションを提供します。

典型的な実装

ステップ

組み合わせパターンへの鍵は、葉とコンテナの両方を表すことができる抽象的なコンポーネントクラスの定義は、抽象的なコンポーネントのためのクライアントのプログラミングは、それが葉やコンテナであるかどうかを知る必要はありません、同時に、コンテナオブジェクトと抽象的なコンポーネントは、コンテナオブジェクトの間の関係の集約の間に確立する必要がある葉とコンテナの両方を含めることができますので、ツリー構造の形成の再帰的な組み合わせを達成するために。

まず、抽象的なビルディング・ブロック・クラスを定義する必要があります:

  • 抽象成果物の定義:抽象成果物クラスを定義し、4つの基本メソッドを追加します:メンバーの追加/削除/取得+ビジネスメソッド、抽象クラスまたはインタフェースとして抽象成果物クラスを定義することができます。
  • リーフアーテファクトの定義:抽象アーテファクトクラスを継承または実装し、具体的なビジネスメソッドをオーバーライドまたは実装し、子アーテファクトのメソッドを管理またはアクセスするための例外処理またはエラーアラートを提供します。
  • コンテナアーティファクトの定義: 抽象アーテファクトクラスを継承または実装し、抽象アーテファクト内のすべてのメソッドをオーバーライドまたは実装します。一般的に、コンテナアーティファクトには、抽象アーテファクトを保存するために使用されるプライベートメンバーのコレクションが含まれます。

抽象コンポーネント

抽象的なビルディング・ブロックは、一般的に以下のように定義されます:

abstract class Component
{
 abstract void add(Component c);
 abstract void remove(Component c);
 abstract Component getChild(int i);
 abstract void operation();
}

リーフアーティファクト

class Leaf extends Component
{
 public void add(Component c)
 {
 //リーフアーティファクトはこのメソッドにアクセスできない
 System.out.println("コンポーネント追加メソッドにアクセスできない。!");
 }
 public void remove(Component c)
 {
 //リーフアーティファクトはこのメソッドにアクセスできない
 System.out.println("削除コンポーネント・メソッドにアクセスできない。!");
 }
 public Component getChild(int i)
 {
 //リーフアーティファクトはこのメソッドにアクセスできない
 System.out.println("エラー、get componentメソッドにアクセスできない。!");
 return null;
 }
 public void operation()
 {
 System.out.println("リーフ・ビジネス・メソッド");
 }
}

リーフ・アーティファクトは、特定のビジネス・メソッド・オプションをオーバーライドするだけでよく、そのメソッドの子アーティファクトの管理でエラーが発生したり、例外がスローされたりしても対処できます。

コンテナアーティファクト

class Composite extends Component
{
 private ArrayList<Component> list = new ArrayList<>();
 
 public void add(Component c)
 {
 list.add(c);
 }
 public void remove(Component c)
 {
 list.remove(c);
 }
 public Component getChild(int i)
 {
 return list.get(i);
 }
 public void operation()
 {
		list.forEach(Component::operation);
 }
}

コンテナアーティファクトは、子アーティファクトを管理するためのメソッドを実装するだけでよく、ビジネスメソッドについては、一般に、再帰的な呼び出しを実現するために抽象アーティファクトのコレクションをトラバースする必要があります。

クライアント

クライアントは抽象的な人工物に対してプログラムを作成し、必要に応じて葉やコンテナを追加します:

public static void main(String[] args) 
{
 Component leaf1 = new Leaf();
 Component leaf2 = new Leaf();
 Component composite1 = new Composite();
 Component composite2 = new Composite();
 composite1.add(leaf1);
 composite2.add(leaf2);
 composite1.add(composite2);
 composite1.operation();
}

インスタンス

フォルダや単一のファイルを殺すことができるアンチウイルスソフトウェアシステムを開発し、また、ファイルの種類によって、例えば、テキストファイルとイメージファイルの殺し方に違いがあるなど、様々な殺し方を提供し、モードを組み合わせてシステムを設計します。

デザインは以下の通り:

  • 抽象コンポーネントクラス: AbstractFile
  • コンテナコンポーネントクラス: Folder
  • リーフコンポーネントのクラス: ImageFile+TextFile+VideoFile

コードは以下の通り:

public class Test
{
 public static void main(String[] args) {
 AbstractFile file1,file2,file3,file4,folder1,folder2;
 file1 = new ImageFile("イメージファイル#1");
 file2 = new VideoFile("ドキュメント#1");
 file3 = new TextFile("テキストファイル#1");
 file4 = new ImageFile("イメージファイル#2");
 folder1 = new Folder("フォルダ1");
 folder2 = new Folder("フォルダ2");
 try
 {
 folder2.add(file1);
 folder2.add(file2);
 folder2.add(file3);
 folder1.add(file4);
 folder1.add(folder2);
 }
 catch(IllegalAccessException e)
 {
 e.printStackTrace();
 }
 
 folder1.killVirus();
 System.out.println();
 folder2.killVirus();
 }
}
//抽象コンポーネントクラス
abstract class AbstractFile
{
 protected String name;
 abstract void add(AbstractFile file) throws IllegalAccessException;
 abstract void remove(AbstractFile file) throws IllegalAccessException;
 abstract AbstractFile getChild(int i) throws IllegalAccessException;
 public void killVirus()
 {
 System.out.println(name+" アンチウイルス");
 }
}
//リーフ・アーティファクト・クラス
class ImageFile extends AbstractFile
{
 public ImageFile(String name)
 {
 this.name = name;
 }
 public void add(AbstractFile c)
 {
 throw new IllegalAccessException("ファイル追加メソッドにアクセスできない!");
 }
 public void remove(AbstractFile c)
 {
 throw new IllegalAccessException("ファイル削除メソッドにアクセスできない!");
 }
 public AbstractFile getChild(int i)
 {
 throw new IllegalAccessException("ファイル取得メソッドにアクセスできない!");
 }
}
//リーフ・アーティファクト・クラス
class TextFile extends AbstractFile
{
 public TextFile(String name)
 {
 this.name = name;
 }
 public void add(AbstractFile c)
 {
 throw new IllegalAccessException("ファイル追加メソッドにアクセスできない!");
 }
 public void remove(AbstractFile c)
 {
 throw new IllegalAccessException("ファイル削除メソッドにアクセスできない!");
 }
 public AbstractFile getChild(int i)
 {
 throw new IllegalAccessException("ファイル取得メソッドにアクセスできない!");
 }
}
//リーフ・アーティファクト・クラス
class VideoFile extends AbstractFile
{
 public VideoFile(String name)
 {
 this.name = name;
 }
	public void add(AbstractFile c)
 {
 throw new IllegalAccessException("ファイル追加メソッドにアクセスできない!");
 }
 public void remove(AbstractFile c)
 {
 throw new IllegalAccessException("ファイル削除メソッドにアクセスできない!");
 }
 public AbstractFile getChild(int i)
 {
 throw new IllegalAccessException("ファイル取得メソッドにアクセスできない!");
 }
}
//コンテナ・コンポーネント・クラス
class Folder extends AbstractFile
{
 private ArrayList<AbstractFile> list = new ArrayList<>();
 public Folder(String name)
 {
 this.name = name;
 }
 public void add(AbstractFile c)
 {
 list.add(c);
 }
 public void remove(AbstractFile c)
 {
 list.remove(c);
 }
 public AbstractFile getChild(int i)
 {
 return list.get(i);
 }
 public void killVirus()
 {
 System.out.println(" +name+" アンチウイルスを実行する");
 list.forEach(AbstractFile::killVirus);
 }
}

出力は以下の通り:

透明で安全な組み合わせモデル

コードを単純化する方法

コンビナトリアルパターンがうまく拡張され、上記の例で新しいファイルタイプを追加しても、元のコードを修正する必要はありませんが、抽象コンポーネントクラスAbstractFileがリーフコンポーネントとは関係のないコンポーネント管理メソッドを宣言しているため、リーフコンポーネントとは関係のないコンポーネント管理メソッドを実装する必要があり、多くの繰り返し作業が発生します。

解決策は2つあります:

  • 抽象アーティファクトはデフォルト実装を提供:リーフアーティファクトのアーティファクト管理メソッドは、デフォルト実装を提供するために抽象アーティファクトに転送されます。
  • 抽象的なアーティファクトの除去方法:抽象的なアーティファクトでは、アーティファクトを管理する方法は提供されていません。

デフォルトの実装

抽象アーテファクトを使用してデフォルトの実装メソッドを提供する場合、上記のコード例は次のように簡略化されます:

abstract class AbstractFile
{
 protected String name;
 public AbstractFile(String name)
 {
 this.name = name;
 }
 public void add(AbstractFile file) throws IllegalAccessException
 {
 throw new IllegalAccessException("ファイル追加メソッドにアクセスできない!");
 }
 public void remove(AbstractFile file) throws IllegalAccessException
 {
 throw new IllegalAccessException("ファイル削除メソッドにアクセスできない!");
 }
 public AbstractFile getChild(int i) throws IllegalAccessException
 {
 throw new IllegalAccessException("ファイル取得メソッドにアクセスできない!");
 }
 public void killVirus()
 {
 System.out.println(name+" アンチウイルス");
 }
}
class ImageFile extends AbstractFile
{
 public ImageFile(String name)
 {
 super(name);
 }
}
class TextFile extends AbstractFile
{
 public TextFile(String name)
 {
 super(name);
 }
}
class VideoFile extends AbstractFile
{
 public VideoFile(String name)
 {
 super(name);
 }
}

リーフ・アーティファクトにはコンストラクタ・メソッドしかありません。この修正はコードを単純化しますが、一般的にはリーフ・アーティファクトにこれらのメソッドを提供する意味はありません。なぜなら、リーフはオブジェクトの次のレベルに行かないからです。

削除メソッド

抽象的なアーティファクト削除法を用いて簡略化したコードを実行すると、上記の例は次のように簡略化されます:

abstract class AbstractFile
{
 protected String name;
 public AbstractFile(String name)
 {
 this.name = name;
 }
 abstract void killVirus();
}
class ImageFile extends AbstractFile
{
 public ImageFile(String name)
 {
 super(name);
 }
 public void killVirus()
 {
 System.out.println("イメージファイル"+name+"アンチウイルス");
 }
}
class TextFile extends AbstractFile
{
 public TextFile(String name)
 {
 super(name);
 }
 public void killVirus()
 {
 System.out.println("テキストファイル"+name+"アンチウイルス");
 }
}
class VideoFile extends AbstractFile
{
 public VideoFile(String name)
 {
 super(name);
 }
 public void killVirus()
 {
 System.out.println(" +name+"アンチウイルス");
 }
}

こうすることで、リーフアーテファクトは管理アーテファクトのメソッドにアクセスできなくなりますが、クライアントが抽象アーテファクトクラスAbstractFileのプログラミングを統一できないというデメリットがあります:

AbstractFile file1,file2,file3,file4,folder1,folder2;

AbstractFileからManageConstructorメソッドが削除されたため、クライアントは以下のようにコードを修正する必要があります:

AbstractFile file1,file2,file3,file4;
Folder folder1,folder2;

透過的な構成パターン

Transparent Compositionパターンは、最初の解決策のアプローチで、コンポーネントを管理するために使用されるすべてのメソッドが抽象的な人工物の中で宣言されます。これは、次の構造に示すように、すべてのコンポーネントクラスが同じインターフェースを持つことを保証し、クライアントが抽象的な人工物に対して一様にプログラムできるという利点があります:

なぜなら、リーフオブジェクトとコンテナオブジェクトは根本的に異なるからです。リーフオブジェクトは次のレベルのオブジェクトを持つことができず、ビルディングブロックを管理するメソッドを提供することは無意味です。

セキュアな構成パターン

セーフコンビネーションパターンは2番目の手法のアプローチで、抽象アーティファクトが管理アーティファクトのメソッドを宣言しない代わりに、管理アーティファクトのメソッドをコンテナアーティファクトに追加します。構造を以下に示します:

セキュアコンポジションパターンの欠点は、リーフアーティファクトとコンテナアーティファクトが異なるメソッドを持ち、管理アーティファクトのメソッドはコンテナアーティファクトで定義されるため、透明性が十分でないことです。

主な利点

  • 階層制御: 組み合わせパターンは、オブジェクト階層のすべてまたは一部を表す、階層的な複合オブジェクトを明確に定義することができます。これにより、クライアントは階層の違いを無視することができ、階層全体の制御が容易になります。
  • アーティファクトの一貫した使用:クライアントはコンテナ・アーティファクトまたはリーフ・アーティファクトを一貫して使用できます。
  • 優れた拡張性:オープン&クローズの原則に沿って、新しいコンテナ・コンポーネントやリーフ・コンポーネントを簡単に追加できます。
  • ツリー構造に効果的: Combinatorial Patternは、ツリー構造のオブジェクト指向実装に柔軟なソリューションを提供します。リーフコンポーネントとコンテナコンポーネントの再帰的な組み合わせにより、複雑なツリー構造を形成することができますが、ツリー構造の制御は非常に簡単です。

主な欠点

  • 例えば、コンテナ・コンポーネントに特定の種類のリーフ・コンポーネント、例えばイメージだけを格納できるフォルダだけを持たせたい場合、組合せパターンを使用すると、型システムによる制約に頼ることができず、実行時に型チェックを行う必要があり、より複雑な処理になります。

シナリオ

  • ホールとパーツを持つ階層構造では、ホールとパーツの違いを無視して、クライアントが一貫性を持って扱えるようにすることが望ましい。
  • ツリー構造の取り扱い
  • リーフ・コンポーネントとコンテナ・コンポーネントはシステムから分離でき、固定タイプではないため、新しいリーフ・コンポーネントやコンテナ・コンポーネントを追加する必要があります。

Read next

訪問者の本当のIPを取得する7つの方法、速習!!

多くの場合、ウェブサイトへのアクセスは、単にユーザーのブラウザからサーバーに直接ではなく、途中でCDN、WAF、高防御を展開することができます。例えば、このアーキテクチャでは、「ユーザー > CDN/WAF/High Defence > ソースサーバー」となります。では、プロキシを何重にも重ねた後、サーバはどのようにしてリクエストを開始した本当のクライアントのIPを取得するのでしょうか? 透過的なプロキシサーバーは、ユーザーのHTTP...

May 26, 2020 · 5 min read