【Laravel】Decoratorパターンを使ってコーヒーショップを実装する

2023/01/29に公開

はじめに

今回はデザインパターンの一つであるDecoratorパターンを使って実装していきます。
Decoratorとは装飾者という意味で、元になるオブジェクトに装飾を加えていくことで機能の拡張をすることができます。
Decoratorパターンを適用することで、既存のクラスを変更せずに機能を追加することができるので、変更が少なくなり、メンテナンス性が高くなります。

以下のようなクラス図になります。


[引用] https://ja.wikipedia.org/wiki/Decorator_パターン

環境

参考

https://www.oreilly.co.jp/books/9784873119762/
https://refactoring.guru/ja/design-patterns/decorator
https://ja.wikipedia.org/wiki/Decorator_パターン

今回の実装について

  • コーヒーショップを想定
  • 飲み物はブレンドコーヒーとエスプレッソ
  • トッピングはモカ、豆乳、ホイップの3種類
  • 飲み物とトッピングの説明、料金を表示する
  • 飲み物とトッピングの組み合わせは自由

クラス図

①インターフェースを作成

まずは飲み物の基本となるDrinkインターフェースを作成します。

Drink.php
interface Drink
{
    public function getDescription(): string;

    public function getCost(): int;
}

②インターフェースを実装するクラスを作成

先ほどのDrinkインターフェースを実装するBlendCoffeeクラスとEspressoクラスを作成します。
これらが装飾対象となる飲み物実装クラスです。

BlendCoffee.php
class BlendCoffee implements Drink
{
    public function getDescription(): string
    {
        return "ブレンドコーヒー";
    }

    public function getCost(): int
    {
        return 200;
    }
}
Espresso.php
class Espresso implements Drink
{
    public function getDescription(): string
    {
        return "エスプレッソ";
    }

    public function getCost(): int
    {
        return 250;
    }
}

③トッピング用の抽象クラスを作成

次にトッピング用の抽象クラスを作成します。
飲み物実装クラス(BlendCoffee、Espresso)と交換可能にする必要があるため、Drinkインターフェースを実装します。
各トッピングクラスが装飾する飲み物をインスタンス変数として持ちます。
装飾する飲み物オブジェクトをコンストラクタに渡すことで、インスタンス変数に飲み物オブジェクトを保持します。

ToppingDecorator.php
abstract class ToppingDecorator implements Drink
{
    protected $drink;

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

④各トッピングクラスを作成

先ほどの抽象クラスを継承し、各トッピングクラスを作成します。
ここでは、飲み物の説明と料金に加えて各トッピングの説明と料金を返しています。
保持している飲み物オブジェクトのgetDescription()メソッドを呼び出してから、各トッピングの説明を返します。
料金についても同様です。

MochaDecorator.php
class MochaDecorator extends ToppingDecorator
{
    public function getDescription(): string
    {
        return $this->drink->getDescription() . "、モカ";
    }

    public function getCost(): int
    {
        return $this->drink->getCost() + 50;
    }
}
SoyDecorator.php
class SoyDecorator extends ToppingDecorator
{
    public function getDescription(): string
    {
        return $this->drink->getDescription() . "、豆乳";
    }

    public function getCost(): int
    {
        return $this->drink->getCost() + 100;
    }
}
WhipDecorator.php
class WhipDecorator extends ToppingDecorator
{
    public function getDescription(): string
    {
        return $this->drink->getDescription() . "、ホイップ";
    }

    public function getCost(): int
    {
        return $this->drink->getCost() + 30;
    }
}

⑤コマンド作成

結果を出力するためのコマンドを作成します。

コマンドを入力してCoffeeShopCommand.phpを作成します。
$ sail artisan make:command CoffeeShopCommand

以下のように編集します。
エスプレッソにモカトッピングと、ブレンドコーヒーにホイップ、豆乳をトッピングした結果を出力します。

CoffeeShopCommand.php

class CoffeeShopCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'coffeeShop';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'コーヒーショップの注文内容を出力します。';

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        // エスプレッソにモカをトッピング
        $drink = new MochaDecorator(new Espresso());

        $this->displayInfo($drink);

        // ブレンドコーヒーにホイップ、豆乳をトッピング
        $drink2 = new SoyDecorator(new WhipDecorator(new BlendCoffee()));

        $this->displayInfo($drink2);

        return Command::SUCCESS;
    }

    public function displayInfo(Drink $drink)
    {
        $this->info($drink->getDescription() . ":" . number_format($drink->getCost()) . "円");
    }
}

出力結果は以下のようになります。

% sail artisan coffeeShop    
エスプレッソ、モカ:300円
ブレンドコーヒー、ホイップ、豆乳:330円

このようにDecoratorパターンを適用することで、トッピングを柔軟に実装することができるようになりました。
シロップやチョコソースを追加することになっても抽象クラスを継承して、クラスを追加するだけで済み、既存のコードを修正する必要がないことがわかっていただけると思います。

さいごに

簡易的ではありますが、今回はデザインパターンの一つであるDecoratorパターンについて書かせていただきました。
少しでもお役に立てれば幸いです。
最後までお読みいただきありがとうございました。

Discussion