Decoratorパターン

商品のパターンを増やそう!

コーヒーの配達を行うECサイトで、アイスコーヒーとホットコーヒー、アメリカンだけだった商品ラインナップに対し、ある日、クリームトッピングや、ハニートッピング、マシュマロトッピングなどの組み合わせメニューを行う要望が出てきました。

バリエーションの数だけクラスを作るのは手間がかかりすぎます。
インスタンス変数を増やしてもいいが、料金計算が面倒になりそうです。
なにか良い方法は無いでしょうか?

Decoratorパターンとは

あるクラスにクラスを「かぶせて」機能、もしくは属性を追加するパターンです。

使ってみよう

まずはトッピングがない状態のラインナップを表現してみます。

/**
 * 商品.
 */
class Product
{
  protected int $price;
  protected string $name;

  function getPrice(): int
  {
    return $this->price;
  }
  function getName(): string
  {
    return $this->name;
  }
}

// アイスコーヒー
class IceCoffee extends Product
{
  function IceCoffee()
  {
    $this->price = 400;
    $this->name = 'アイスコーヒー';
  }
}

// ホットコーヒー
class HotCoffee extends Product
{
  function HotCoffee()
  {
    $this->price = 400;
    $this->name = 'ホットコーヒー';
  }
}

// アメリカン
class AmericanCoffee extends Product
{

  function AmericanCoffee()
  {
    $this->price = 500;
    $this->name = 'アメリカンコーヒー';
  }
}

ここで、例えばシンプルにクリームトッピングのアイスコーヒーを表現するとこうなります.

// アイスコーヒー
class CreamedIceCoffee extends Product
{
  function CreamedIceCoffee()
  {
    $this->price = 550;
    $this->name = 'アイスコーヒー クリームトッピング';
  }
}

しかし、このように一つ一つクラスを作るのは手間がかかりますし、
トッピングが増えれば増えるほど手間が大きく、その分バグの可能性も増えてしまいます。

そこで、Decoratorパターンでコーヒーをトッピングでラップするようにしてみましょう。

/**
 * 商品.
 */
class Product
{
  protected int $price;
  protected string $name;

  function getPrice(): int
  {
    return $this->price;
  }
  function getName(): string
  {
    return $this->name;
  }
}

// アイスコーヒー
class IceCoffee extends Product
{
  function IceCoffee()
  {
    $this->price = 400;
    $this->name = 'アイスコーヒー';
  }
}

// ホットコーヒー
class HotCoffee extends Product
{
  function HotCoffee()
  {
    $this->price = 400;
    $this->name = 'ホットコーヒー';
  }
}

// アメリカン
class AmericanCoffee extends Product
{

  function AmericanCoffee()
  {
    $this->price = 500;
    $this->name = 'アメリカンコーヒー';
  }
}

//
class CreamedCoffee extends Product
{
  protected Product $baseProduct;
  function CreamedCoffee(Product $product)
  {
    $this->baseProduct = $product;
    $this->price = 150;
    $this->name = 'クリームトッピング';
  }

  function getPrice(): int
  {
    return $this->price + $baseProduct->getPrice();
  }
  function getName(): string
  {
    return $baseProduct->getName() . ' ' . $this->name;
  }
}

//
class HoneyCoffee extends Product
{
  protected Product $baseProduct;
  function HoneyCoffee(Product $product)
  {
    $baseProduct = $product;
    $this->price = 200;
    $this->name = 'ハニートッピング';
  }

  function getPrice(): int
  {
    return $this->price + $baseProduct->getPrice();
  }
  function getName(): string
  {
    return $baseProduct->getName() . ' ' . $this->name;
  }
}

//
class MarshmallowCoffee extends Product
{
  protected Product $baseProduct;
  function MarshmallowCoffee(Product $product)
  {
    $baseProduct = $product;
    $this->price = 250;
    $this->name = 'マシュマロトッピング';
  }

  function getPrice(): int
  {
    return $this->price + $baseProduct->getPrice();
  }
  function getName(): string
  {
    return $baseProduct->getName() . ' ' . $this->name;
  }
}

このようにすればあらゆる組み合わせに対応できる上、トッピングが増えてもトッピングそのもののクラスを作成するだけで対応できます。
※ わかりやすくするために、トッピングクラスに関してはProductクラスを継承するだけにしています。そのため、getPrice, getNameが冗長になっています。
※ トッピング用のクラスを作成して、getPrice(), getName()を共通化しても良いでしょう。

何かを組み合わせるような場合、Decoratorパターンを検討してみると良いでしょう。