RitoLabo

Compositeパターン - PHPデザインパターン

  • 公開:
  • カテゴリ: PHP DesignPatterns
  • タグ: PHP,DesignPatterns,Structure,Composite

Compositeパターン(コンポジット・パターン)は構造に関するデザインパターン手法の1つで、入れ物とその中身を同一のものと取り、再帰的な構造での取り扱いを簡単にする処理モデルです。

これにはツリー構造、例えば枝と葉をイメージすると理解しやすいです。「葉」は枝に付いている(枝に属する)ものであり、葉はそれだけを出力するが、枝は自身と併せて葉たちも出力する。という関係性を以て構造を構築する事で、子孫構造を気にする事なく、全ての枝葉を処理出来る。という事になります。

Compositeパターンの基本的なクラス図は以下になります。

Compositeパターンクラス図

実装例

今回は、ツリー構造の代表的な例としてファイルとディレクトリの関係で実装します。

ご存知の通り、ファイルシステムでは、ルートディレクトリがあり、その中にファイルがあり、ディレクトリもあり、そのディレクトリの中にまたファイルやディレクトリが存在する。というツリー構造になっています。これらをCompositeパターンで実装していきます。

まずはディレクトリ、ファイルどちらでも共通にアクセスするためのAPIであるComponentクラスを作成します。

App/Component/FileSystem.php
<?php

namespace App\Component;

abstract class FileSystem
{
private $name;

public function __construct($name)
{
$this->name = $name;
}

public function getName()
{
return $this->name;
}

public abstract function add(FileSystem $file);

public function display()
{
echo sprintf("%s<br>\n", $this->getName());
}
}
  • コンストラクタで名前を登録しています。
  • getName() メソッドで名前を取得できます。
  • add() はファイル集合へ追加するメソッドですが、枝には必要でも葉には不要なので抽象化させ、継承先で実装させる流れになっています。
  • display() メソッドはFileSystemオブジェクトで保持している名前を出力します。

次に、枝を司るcompositeクラスを作成します。この例では、ディレクトリにあたります。

App/Composite/Dir.php
<?php

namespace App\Composite;

use App\Component\FileSystem;

class Dir extends FileSystem
{
private $files;

public function __construct($name)
{
parent::__construct($name);
$this->files = [];
}

public function add(FileSystem $file)
{
array_push($this->files, $file);
}

public function display()
{
parent::display();
foreach ($this->files as $file) {
echo $file->display();
}
}
}
  • ディレクトリ、ファイル双方の共通APIであるFileSystemクラスを継承しています。
  • コンストラクタでは名前を登録しますが、FileSystemクラスのコンストラクタでの登録になります。
  • add() メソッドでは、FileSystemクラスを自身のメンバ変数$filesへ追加しています。つまり、ここで枝(ディレクトリ)に葉(ファイル)を追加する事が出来ます。
  • display() メソッドでは、継承元の出力を行いながらも、保持している葉の出力をループで行っています。ここがCompositeパターンのポイントの1つでもあります。

最後に、葉を司るleafクラスを作成します。この例ではファイルにあたります。

App/Leaf/File.php
<?php

namespace App\Leaf;

use App\Component\FileSystem;

class File extends FileSystem
{
public function __construct($name)
{
parent::__construct($name);
}

public function getName()
{
return parent::getName();
}

public function add(FileSystem $file)
{
throw new Exception('This method is not allowed.');
}
}
  • ディレクトリ、ファイル双方の共通APIであるFileSystemクラスを継承しています。
  • コンストラクタでは名前を登録しますが、FileSystemクラスのコンストラクタでの登録になります。
  • getName() メソッドで継承元のメソッドを呼び出し、名前を返却します。
  • add() メソッドは使用しない為、例外を発生させています。(Fileクラスでは追加を行わないため)

一通り実装できたので、クライアントから利用してみます。

index.php
<?php

use App\Composite\Dir;
use App\Leaf\File;

$root_dir = new Dir('/'); // ルートディレクトリ

$file_1 = new File('file_01.txt');
$root_dir->add($file_1);
// => /file_01.txt

$file_2 = new File('file_02.txt');
$root_dir->add($file_2);
// => /file_02.txt

$dir_1 = new Dir('dir01/');
$root_dir->add($dir_1);
// => /dir01/

$dir_2 = new Dir('dir02/');
$dir_2->add(new File('file_03.txt'));
$root_dir->add($dir_2);
// => /dir02/
// => /dir02/file_03.txt

$dir_3 = new Dir('dir03/');
$dir_3->add(new File('file_04.txt'));
$dir_3->add(new File('file_05.txt'));
$dir_3->add(new File('file_06.txt'));

$dir_4 = new Dir('dir04/');
$dir_4->add(new File('file_07.txt'));
$dir_3->add($dir_4);

$root_dir->add($dir_3);
// => /dir03/
// => /dir03/file_04.txt
// => /dir03/file_05.txt
// => /dir03/file_06.txt
// => /dir03/dir04/
// => /dir03/dir04/file_07.txt

// 出力
$root_dir->display();

コメントアウト部分は、どんな風に追加を行っているのかイメージしやすいように書いてあります。つまり、以下のように階層になり格納されているという事です。

App\Composite\Dir Object
(
[files:App\Composite\Dir:private] => Array
(
[0] => App\Leaf\File Object
(
[name:App\Component\FileSystem:private] => file_01.txt
)

[1] => App\Leaf\File Object
(
[name:App\Component\FileSystem:private] => file_02.txt
)

[2] => App\Composite\Dir Object
(
[files:App\Composite\Dir:private] => Array
(
)

[name:App\Component\FileSystem:private] => dir01/
)

[3] => App\Composite\Dir Object
(
[files:App\Composite\Dir:private] => Array
(
[0] => App\Leaf\File Object
(
[name:App\Component\FileSystem:private] => file_03.txt
)

)

[name:App\Component\FileSystem:private] => dir02/
)

[4] => App\Composite\Dir Object
(
[files:App\Composite\Dir:private] => Array
(
[0] => App\Leaf\File Object
(
[name:App\Component\FileSystem:private] => file_04.txt
)

[1] => App\Leaf\File Object
(
[name:App\Component\FileSystem:private] => file_05.txt
)

[2] => App\Leaf\File Object
(
[name:App\Component\FileSystem:private] => file_06.txt
)

[3] => App\Composite\Dir Object
(
[files:App\Composite\Dir:private] => Array
(
[0] => App\Leaf\File Object
(
[name:App\Component\FileSystem:private] => file_07.txt
)

)

[name:App\Component\FileSystem:private] => dir04/
)

)

[name:App\Component\FileSystem:private] => dir03/
)

)

[name:App\Component\FileSystem:private] => /
)

これを出力すると、以下になります。

Compositeパターンでの出力結果

自身のオブジェクトによって子孫関係を吸収し出力を行っているので、階層構造を気にする事なく再帰的に出力が行われている事が確認できました。

まとめ

Composite = 複合物・合成物・混合物」を意味する通り、Compositeパターンでは単体のオブジェクト、その集合体どちらであっても同じように処理できるようにするというものになります。

ネストされた関係に規則性を持たない場合などの状況でそれらを処理する必要がある場合に活用できるデザインパターンなので是非試してみてください。