【Laravel】Decoratorパターンを使ってコーヒーショップを実装する
はじめに
今回はデザインパターンの一つであるDecoratorパターンを使って実装していきます。
Decoratorとは装飾者という意味で、元になるオブジェクトに装飾を加えていくことで機能の拡張をすることができます。
Decoratorパターンを適用することで、既存のクラスを変更せずに機能を追加することができるので、変更が少なくなり、メンテナンス性が高くなります。
以下のようなクラス図になります。
[引用] https://ja.wikipedia.org/wiki/Decorator_パターン
環境
- Laravel 9.38.0
- PHP 8.1.12
Laravel Sailで環境構築しています。
https://readouble.com/laravel/9.x/ja/sail.html
参考
今回の実装について
- コーヒーショップを想定
- 飲み物はブレンドコーヒーとエスプレッソ
- トッピングはモカ、豆乳、ホイップの3種類
- 飲み物とトッピングの説明、料金を表示する
- 飲み物とトッピングの組み合わせは自由
クラス図
①インターフェースを作成
まずは飲み物の基本となるDrinkインターフェースを作成します。
interface Drink
{
public function getDescription(): string;
public function getCost(): int;
}
②インターフェースを実装するクラスを作成
先ほどのDrinkインターフェースを実装するBlendCoffeeクラスとEspressoクラスを作成します。
これらが装飾対象となる飲み物実装クラスです。
class BlendCoffee implements Drink
{
public function getDescription(): string
{
return "ブレンドコーヒー";
}
public function getCost(): int
{
return 200;
}
}
class Espresso implements Drink
{
public function getDescription(): string
{
return "エスプレッソ";
}
public function getCost(): int
{
return 250;
}
}
③トッピング用の抽象クラスを作成
次にトッピング用の抽象クラスを作成します。
飲み物実装クラス(BlendCoffee、Espresso)と交換可能にする必要があるため、Drinkインターフェースを実装します。
各トッピングクラスが装飾する飲み物をインスタンス変数として持ちます。
装飾する飲み物オブジェクトをコンストラクタに渡すことで、インスタンス変数に飲み物オブジェクトを保持します。
abstract class ToppingDecorator implements Drink
{
protected $drink;
public function __construct(Drink $drink)
{
$this->drink = $drink;
}
}
④各トッピングクラスを作成
先ほどの抽象クラスを継承し、各トッピングクラスを作成します。
ここでは、飲み物の説明と料金に加えて各トッピングの説明と料金を返しています。
保持している飲み物オブジェクトのgetDescription()
メソッドを呼び出してから、各トッピングの説明を返します。
料金についても同様です。
class MochaDecorator extends ToppingDecorator
{
public function getDescription(): string
{
return $this->drink->getDescription() . "、モカ";
}
public function getCost(): int
{
return $this->drink->getCost() + 50;
}
}
class SoyDecorator extends ToppingDecorator
{
public function getDescription(): string
{
return $this->drink->getDescription() . "、豆乳";
}
public function getCost(): int
{
return $this->drink->getCost() + 100;
}
}
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
以下のように編集します。
エスプレッソにモカトッピングと、ブレンドコーヒーにホイップ、豆乳をトッピングした結果を出力します。
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