RitoLabo

Decoratorパターン - PHPデザインパターン

  • 公開:
  • カテゴリ: PHP DesignPatterns
  • タグ: PHP,DesignPatterns,Structure,Decorator

Decoratorパターン(デコレーター・パターン)は、構造に関するデザインパターン手法の一つで、基となるオブジェクトとそれを装飾するオブジェクトを同一のレベルで扱えるような関係性を築く事で、より柔軟なパラメータの取り回しや機能拡張を実現できる処理モデルです。

Decoratorパターンの基本的なクラス図は以下の通りです。

Decoratorパターンクラス図

実装例

今回は、旅行の計画を例にしてDecoratorパターンを実装していきます。

旅行に行こうと思い計画を立てる場合、お得なパック(宿と飛行機がセットになっているやつ)があるか良く探したりします。

旅先も慣れていれば結局はパックじゃない方が安くあがったりする(FF5も最終的にはすっぴんが最も強い)ものですが、そういわずに、色々とプランを探してみようと思います。

(5分後)

ネットで色々と探してみた結果、どうやら飛行機のみのパック・ホテルのみのパック・飛行機&ホテルのパックがあるらしい(のみってパックっていうのか?)ので、ちょっとDecoratorパターンであなたにも共有します。

まずは、旅行プランに必要なインターフェイスを宣言します。

App/Interfaces/PlanInterface.php
<?php

namespace App\Interfaces;

interface PlanInterface
{
public function getPlan();
public function setPlan($text);
public function getCost();
public function setCost($number);
}

日本人はとかく連休を取る事を後ろめたく思う人種ですが、決意表明にも近いこのインターフェイスからは「私は必ず旅行に行くんだ」という強い意志を感じます。

それぞれゲッタセッタですが、プランに関するメソッドと、費用に関するメソッドを声高らかに宣言しています。

次に、旅行プランのベースとなるComponentクラスを作成します。先ほど作成したインターフェイスを実装します。

App/Components/TravelPlan.php
<?php

namespace App\Components;

use App\Interfaces\PlanInterface;


class TravelPlan implements PlanInterface
{
private $plan;
private $cost = 0;
private $duration;

public function getPlan()
{
return $this->plan;
}
public function setPlan($text)
{
$this->plan = $text;
}
public function getCost()
{
return $this->cost;
}
public function setCost($number)
{
$this->cost = $number;
}

public function getDuration()
{
return $this->duration;
}
public function setDuration($duration)
{
$this->duration = $duration;
}

}

インターフェイスの実装の他に、duration(旅行期間)についてのゲッタセッタも定義しました。これで旅行がより現実味を帯びてきました。

次に、このデザインパターン手法の肝でもある、Decoratorを作成します。ベースとなる旅行プランを装飾するクラスです。まずはDecoratorクラスから。

App/Decorators/PlanDecorator.php
<?php

namespace App\Decorators;

use App\Interfaces\PlanInterface;

abstract class PlanDecorator implements PlanInterface
{
private $obj;

public function __construct(PlanInterface $obj)
{
$this->obj = $obj;
}
public function getPlan()
{
return $this->obj->getPlan();
}
public function setPlan($text)
{
$this->obj->setPlan($text);
}
public function getCost()
{
return $this->obj->getCost();
}
public function setCost($number)
{
$this->obj->setCost($this->getCost() + $number);
}
}

Componentクラスと同じインターフェイスを実装しています。また、コンストラクタではインスタンスを受け取っています。彼が次に作成するDecoratorの親クラスになります。

次は、Decoratorの具象クラスです。飛行機パックに関するクラスと、ホテルパックに関するクラスを作成していきます。

App/Decorators/Packs/AirplanePack.php
<?php

namespace App\Decorators\Packs;

use App\Decorators\PlanDecorator;
use App\Interfaces\PlanInterface;

class AirplanePack extends PlanDecorator
{
private $airlines; // 航空会社
private $additional_cost = 0; // 追加料金

public function __construct(PlanInterface $obj, $airlines, $additional_cost)
{
parent::__construct($obj);
$this->airlines = $airlines;
$this->additional_cost = $additional_cost;
}

public function getPlan()
{
return sprintf('%s / 飛行機パック(%s', parent::getPlan(), $this->airlines);
}

public function getCost()
{
return parent::getCost() + $this->additional_cost;
}
}
App/Decorators/Packs/HotelPack.php
<?php

namespace App\Decorators\Packs;

use App\Decorators\PlanDecorator;
use App\Interfaces\PlanInterface;

class HotelPack extends PlanDecorator
{
private $hotel_name; // ホテル名
private $additional_cost = 0; // 追加料金

public function __construct(PlanInterface $obj, $hotel_name, $additional_cost)
{
parent::__construct($obj);
$this->hotel_name = $hotel_name;
$this->additional_cost = $additional_cost;
}

public function getPlan()
{
return sprintf('%s / ホテルパック(%s', parent::getPlan(), $this->hotel_name);
}

public function getCost()
{
return parent::getCost() + $this->additional_cost;
}
}

それぞれ、先ほど作成したDecoratorクラスを継承し、コンストラクタで旅行プランオブジェクトと必要なパラメータを受け取っています。

そしてプランと費用の出力メソッドを定義していますが、このクラスで必要な値をそれぞれ付与したり加算したりして返却している点がポイントです。それぞれのクラスで必要な値を保持し、必要に応じて追加するパターンがここのポイントです。(ただし受け渡しを行うかどうかは要件次第です)

実装が一通り完了したので、クライアントから利用してみます。

index.php
<?php

use App\Components\TravelPlan;
use App\Decorators\Packs\AirplanePack;
use App\Decorators\Packs\HotelPack;

$plan_base = new TravelPlan();
$plan_base->setPlan('沖縄');
$plan_base->setDuration('9/20 - 9/25');
echo '<h2>旅行プラン(ベース)</h2>';
echo sprintf('%s %s', $plan_base->getDuration(), $plan_base->getPlan());

$pack_plan_1 = new AirplanePack($plan_base, 'Laravel航空', 50000);
echo '<h2>旅行プラン1 飛行機パック</h2>';
echo $pack_plan_1->getPlan();
echo number_format($pack_plan_1->getCost());

$pack_plan_2 = new HotelPack($plan_base, 'PHPホテル', 67000);
echo '<h2>旅行プラン2 ホテルパック</h2>';
echo $pack_plan_2->getPlan();
echo number_format($pack_plan_2->getCost());

echo '<h2>旅行プラン3 飛行機+ホテルパック</h2>';
$pack_plan_3 = new HotelPack(
new AirplanePack($plan_base, 'Laravel航空', 50000),
'PHPホテル',
67000
);
echo $pack_plan_3->getPlan();
echo number_format($pack_plan_3->getCost());

生成したベースの旅行プランオブジェクトを、それぞれのDecoratorに渡したり、Decoratorクラスでネストしたりしています。つまりは、どれかのパックを適用したり、複数適用したりしています。

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

PHP Decoratorパターン 結果表示

ベースの旅行プランに対して、それぞれのパックが適用された場合の金額などが確認できました。やっぱり高い、パックは無しですね。

まとめ

Decorator は 「装飾者」という意味がある通り、基のオブジェクトをラップする事で機能やその状態の拡張を実現します。そしてラップという形態が、基の状態を維持する事につながり、1つのベースに対して状態のバリエーションを作り出す事も出来ます。

また、実装例から、多重ラップの際に機能(振る舞い)が動的に追加されている事に気づいたでしょうか。ラップにラップしても、同じゲットメソッドで非ラップ時と同じように、しかもラップしただけの状態を動的に追加された状態で取得する事が出来る。これがDecoratorパターンの良いところの一つです。

さてPHPでこのパターンを生かせるのはあとはどんな時でしょう。是非試してみてください。