Menu
Coddy logo textTech

コンポジットパターン

CoddyのPHPジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 80/91。

Composite Pattern(コンポジット・パターン)は、オブジェクトをツリー構造に構成し、それらの構造を個々のオブジェクトであるかのように扱うことができる構造デザインパターンです。単一のオブジェクトとオブジェクトのグループを均一に扱いたい「部分-全体」の階層構造を表現するのに最適です。

ファイルシステムを考えてみましょう。フォルダには、ファイルや他のフォルダを含めることができます。

合計サイズを計算する際、ファイルとフォルダの両方に対して同じメソッドを呼び出し、フォルダはその中身を再帰的に合計するようにしたい場合があります。Compositeパターンは、リーフ(ファイル)とコンポジット(フォルダ)の両方に同じインターフェースを持たせることで、これをシームレスに実現します。

<?php
interface FileSystemItem {
    public function getName(): string;
    public function getSize(): int;
}

class File implements FileSystemItem {
    public function __construct(
        private string $name,
        private int $size
    ) {}
    
    public function getName(): string {
        return $this->name;
    }
    
    public function getSize(): int {
        return $this->size;
    }
}

class Folder implements FileSystemItem {
    private array $items = [];
    
    public function __construct(private string $name) {}
    
    public function getName(): string {
        return $this->name;
    }
    
    public function add(FileSystemItem $item): void {
        $this->items[] = $item;
    }
    
    public function getSize(): int {
        $total = 0;
        foreach ($this->items as $item) {
            $total += $item->getSize();
        }
        return $total;
    }
}

これで、ツリー構造を構築し、それらを一律に扱うことができるようになりました:

<?php
$docs = new Folder("Documents");
$docs->add(new File("resume.pdf", 250));
$docs->add(new File("cover.docx", 50));

$images = new Folder("Images");
$images->add(new File("photo.jpg", 1500));

$root = new Folder("Root");
$root->add($docs);
$root->add($images);
$root->add(new File("readme.txt", 10));

echo $root->getSize();

出力:

1810

クライアントコードは、単一のファイルか、深くネストされたフォルダ構造かを気にすることなく getSize() を呼び出します。このパターンは内部で再帰を処理するため、複雑な階層構造を簡単に扱うことができます。

challenge icon

チャレンジ

簡単

Compositeパターンを使用して、組織図システムを構築しましょう。企業は、部門が従業員や他のサブ部門を含むことができる階層構造を持っており、組織全体で一律に合計を計算したい場合に最適なツリー構造です。

コードは4つのファイルに分けて構成します:

  • OrganizationUnit.php — 個々の従業員と部門の両方が実装する OrganizationUnit インターフェースを定義します。これには、getName(): stringgetSalary(): int の2つのメソッドが必要です。この共通インターフェースにより、コストを計算する際に、単一の従業員と部門全体を同じように扱うことができます。
  • Employee.php — OrganizationUnit インターフェースを読み込み、それを実装する Employee クラスを作成します。従業員は組織ツリーのリーフノード(末端)であり、名前と給与を持ちますが、他のユニットを含むことはできません。コンストラクタのプロパティ昇格(constructor promotion)を使用して名前と給与を受け取り、両方のインターフェースメソッドを実装してください。
  • Department.php — OrganizationUnit インターフェースを読み込み、それを実装する Department クラスを作成します。部門は、従業員と他の部門の両方を含むことができるコンポジット(複合体)です。部門クラスでは以下のことを行います:
    • コンストラクタで名前を受け取る
    • 従業員やサブ部門を追加するための add(OrganizationUnit $unit): void メソッドを持つ
    • getSalary() を実装し、含まれるすべてのユニットの合計給与を(内部のすべてを再帰的に合計して)返す
  • main.php — Employee と Department のファイルを読み込みます。1つの入力として、組織構造を表すJSON文字列を受け取ります。

    JSONは以下の形式です:

    {"Engineering": [{"name": "Alice", "salary": 80000}, {"name": "Bob", "salary": 75000}], "Sales": [{"name": "Carol", "salary": 65000}]}

    各キーは部門名であり、その値は名前と給与を持つ従業員の配列です。

    "Company" という名前のルート Department を作成します。JSON内の各部門について、Department を作成し、その従業員を追加してから、その部門を Company に追加します。最後に、会社の総給与を以下の形式で出力してください:

    Total Salary: [amount]

Compositeパターンの素晴らしい点は、Company 部門に対して getSalary() を呼び出すだけで、ネストされたすべての部門と従業員の合計が自動的に計算されることです。クライアント側のコードは内部構造を知る必要がありません。

チートシート

Compositeパターンは、オブジェクトをツリー構造に構成し、個々のオブジェクトとグループを均一に扱えるようにする構造デザインパターンです。

これは、リーフノードとコンポジットノードの両方に同じインターフェースを使用したい「部分-全体」の階層構造に最適です。

主要な構成要素

  • Componentインターフェース — リーフとコンポジットの両方に共通の操作を定義します
  • Leaf — 他のオブジェクトを含むことができない個々のオブジェクトを表します
  • Composite — 子(リーフまたは他のコンポジット)を含み、子に委譲することで操作を実装します

例:ファイルシステム

<?php
interface FileSystemItem {
    public function getName(): string;
    public function getSize(): int;
}

class File implements FileSystemItem {
    public function __construct(
        private string $name,
        private int $size
    ) {}
    
    public function getName(): string {
        return $this->name;
    }
    
    public function getSize(): int {
        return $this->size;
    }
}

class Folder implements FileSystemItem {
    private array $items = [];
    
    public function __construct(private string $name) {}
    
    public function getName(): string {
        return $this->name;
    }
    
    public function add(FileSystemItem $item): void {
        $this->items[] = $item;
    }
    
    public function getSize(): int {
        $total = 0;
        foreach ($this->items as $item) {
            $total += $item->getSize();
        }
        return $total;
    }
}

使い方

<?php
$docs = new Folder("Documents");
$docs->add(new File("resume.pdf", 250));
$docs->add(new File("cover.docx", 50));

$images = new Folder("Images");
$images->add(new File("photo.jpg", 1500));

$root = new Folder("Root");
$root->add($docs);
$root->add($images);
$root->add(new File("readme.txt", 10));

echo $root->getSize(); // 1810

クライアントは、ファイルとフォルダの両方に対して一様に getSize() を呼び出します。コンポジットが内部で再帰を処理するため、複雑な階層構造を簡単に扱うことができます。

自分で試してみよう

<?php
require_once 'Employee.php';
require_once 'Department.php';

// JSON入力を読み込む
$input = trim(fgets(STDIN));
$data = (array)json_decode($input, true);

// TODO: "Company"という名前のルートDepartmentを作成する

// TODO: JSONデータの各部門をループで処理する
// 各部門について:
//   - 新しいDepartmentオブジェクトを作成する
//   - 各従業員をその部門に追加する
//   - 部門をCompanyに追加する

// TODO: 合計給与を次の形式で出力する: Total Salary: [amount]

?>
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン