Compositeパターン

商品カテゴリのツリーを表示しよう

商品カテゴリを表示したいと考えています。
商品カテゴリは木構造となっています。
早速実装してみましょう。

<?php

function render()
{
  echo "ホーム\n";
  echo " CPU\n";
  echo "  おそいCPU (/cpu/slow_cpu)\n";
  echo "  はやいCPU (/cpu/lapid_cpu)\n";
  echo " メモリ\n";
  echo "  8GB (/memory/8gb)\n";
  echo "  16GB (/memory/16gb)\n";
  echo " ストレージ\n";
  echo "  ハードディスク\n";
  echo "   512GB (/storage/hdd/512gb)\n";
  echo "   1TB (/storage/hdd/1tb)\n";
  echo "  SSD\n";
  echo "   256GB (/storage/ssd/256gb)\n";
  echo "   512GB (/storage/ssd/512gb)\n";
}

render();

単純に固定で書けば良い、という話ですね。
しかし、たとえばハードディスクカテゴリとSSDカテゴリをストレージカテゴリと同列にして、ストレージカテゴリを配したり、
逆に、ハードディスクカテゴリの中に更にカテゴリを増やすなど柔軟にカテゴリを構築し直すには固定では手間がかかりすぎます。

そんなときには、Compositeパターンを使ってみましょう。

Compositeパターンとは

再帰的な構造を表現するパターンです。

使ってみよう

<?php
interface CategoryEntry
{
  public function getChildren(): array;
  public function getLink(): string;
  public function getLabel(): string;
}

class CategoryNode implements CategoryEntry
{
  private string $label;
  private array $children;
  function CategoryNode(string $label)
  {
    $this->label = $label;
    $this->children = [];
  }
  public function getChildren(): array
  {
    return $this->children;
  }
  public function getLabel(): string
  {
    return $this->label;
  }
  public function addEntry(CategoryEntry $entry): void
  {
    $this->children[] = $entry;
  }
  public function getLink(): string
  {
    return (string) null;
  }
}

class CategoryLeaf implements CategoryEntry
{
  private string $label;
  private string $link;
  public function CategoryLeaf(string $label, string $link)
  {
    $this->label = $label;
    $this->link = $link;
  }
  public function getChildren(): array
  {
    return (array) null;
  }
  public function getLabel(): string
  {
    return $this->label;
  }
  public function getLink(): string
  {
    return $this->link;
  }
}

class CategoryManager
{

  public function getCategoryTree(): CategoryNode
  {
    $root = new CategoryNode('ホーム');

    $cpu = new CategoryNode('CPU');
    $cpu->addEntry(new CategoryLeaf('おそいCPU', '/cpu/slow_cpu'));
    $cpu->addEntry(new CategoryLeaf('はやいCPU', '/cpu/lapid_cpu'));

    $memory = new CategoryNode('メモリ');
    $memory->addEntry(new CategoryLeaf('8GB', '/memory/8gb'));
    $memory->addEntry(new CategoryLeaf('16GB', '/memory/16gb'));

    $hdd = new CategoryNode('ハードディスク');
    $hdd->addEntry(new CategoryLeaf('512GB', '/storage/hdd/512gb'));
    $hdd->addEntry(new CategoryLeaf('1TB', '/storage/hdd/1tb'));
    $ssd = new CategoryNode('SSD');
    $ssd->addEntry(new CategoryLeaf('256GB', '/storage/ssd/256gb'));
    $ssd->addEntry(new CategoryLeaf('512GB', '/storage/ssd/512gb'));
    $storage = new CategoryNode('ストレージ');
    $storage->addEntry($hdd);
    $storage->addEntry($ssd);

    $root->addEntry($cpu);
    $root->addEntry($memory);
    $root->addEntry($storage);

    return $root;
  }
}

function render(CategoryEntry $entry, int $depth = 0)
{
  if ($entry == null) {
    return;
  }

  $indent = '';
  for ($i = 0; $i < $depth; $i++) {
    $indent .= ' ';
  }

  echo $indent . $entry->getLabel() . ($entry->getLink() ? ' (' . $entry->getLink() . ')' : '')  . "\n";

  $children = $entry->getChildren();
  $depth++;
  foreach ($children as $child) {
    render($child, $depth);
  }
}

$categoryManager = new CategoryManager();
$root = $categoryManager->getCategoryTree();

render($root);

Compositeパターンを使えば、再帰的な構造を表現できるので、構造の組み換えなどが容易にできます。
また、オブジェクト同士の包含関係としてデータを持つので、テキストで直接表現するのに比べて再帰的構造が崩れて表現されるようなリスクが極めて低くなります。

フォルダ構造のような再帰的な構造を持つデータがあり、頻繁に構造が変わることがあるようなときにはCompositeパターンを検討すると良いでしょう。