① Strategyパターンを使ってみる

最近デザインパターンを使いこなせるようになろうと奮闘しております。第一回目はStrategyパターンを PHP7.4 で実装してみたいと思います。

参考書籍は「Head Firstデザインパターン ―頭とからだで覚えるデザインパターンの基本」です。

strategyパターンは振る舞いに関してのパターンのうちの一つになります。他には生成に関するパターンと構造に関するパターンがあります。

Head Firstデザインパターン でのStrategyパターンの定義は以下です。

一連のアルゴリズムを定義し、それぞれをカプセル化してそれぞれを交換可能にする。Strategyパターンによって、アルゴリズムを使用するクライアントとは独立してアルゴリズムを使用するクライアントとは独立して、アルゴリズムを使用する 。

…うん。

試しにちょっとサンプルを作ってみます。
サービスの利用人数に対してプランA,B,Cの月額料金の見積もりを表示するプログラムを書いていきたいと思います。

条件
プランA : 利用人数一人につき1000円
プランB : 利用人数一人につき1500円 ※5人以上利用する場合は1割引きになる
プランC : 利用人数一人につき2000円 ※人数による料金とは別に月額で10000円かかる

まずはパターンを使わずにごり押しで書いてみます。

<?php
class EstimatePlan
{
    const CHARGE_PER_PEOPLE_PLAN_A  = 1000;
    const CHARGE_PER_PEOPLE_PLAN_B  = 1500;
    const CHARGE_PER_PEOPLE_PLAN_C  = 2000;
    const MONTHLY_LICENCE_CHARGE_PLAN_C = 10000;

    private string $name;
    private int $numberOfPeople;

    public function __construct(int $numberOfPeople, string $name)
    {
        $this->numberOfPeople = $numberOfPeople;
        $this->name = $name;
    }

    public function getName() : string
    {
        return $this->name;
    }

    public function getChargeForPeople() : int
    {
        $value = null;

        if ($this->name === 'a') {
            $value = self::CHARGE_PER_PEOPLE_PLAN_A * $this->numberOfPeople;
        } elseif ($this->name === 'b') {
            $value = self::CHARGE_PER_PEOPLE_PLAN_B * $this->numberOfPeople;
        } elseif ($this->name === 'c') {
            $value = self::CHARGE_PER_PEOPLE_PLAN_C * $this->numberOfPeople;
        } else {
            //例外
        }

        return $value;
    }

    public function getSum()
    {
        $value = null;

        if ($this->name === 'a') {
            $value = $this->getChargeForPeople();
        } elseif ($this->name === 'b') {
            $value = $this->getChargeForPeople();
            if ($this->numberOfPeople > 4) {
                $value = $value * 0.9;
            }
        } elseif ($this->name === 'c') {
            $value = $this->getChargeForPeople() + self::MONTHLY_LICENCE_CHARGE_PLAN_C;
        } else {
            //例外
        }

        return $value;
    }
}
<?php
require_once('EstimatePlan.php');

// 引数はユーザーからの入力を想定した利用人数
$numberOfPeople = 5;

$planA = new EstimatePlan($numberOfPeople, 'a');
$planB = new EstimatePlan($numberOfPeople, 'b');
$planC = new EstimatePlan($numberOfPeople, 'c');

echo "<pre>"; print_r('利用人数:'. $numberOfPeople); echo "</pre>";
echo "<pre>"; print_r('プランAの場合の料金:' . $planA->getSum(). '円'); echo "</pre>";
echo "<pre>"; print_r('プランBの場合の料金:' . $planB->getSum(). '円'); echo "</pre>";
echo "<pre>"; print_r('プランCの場合の料金:' . $planC->getSum(). '円'); echo "</pre>";

どうでしょうか、今回はプランが3つしかないのと条件がそんなになかったのでまだ許容範囲だと思いますが、もし新しくプランDが加わったり、料金体系がもっと複雑になった場合はプランA~Cの挙動にも気を使うことになり神経を使います。テストも全プラン必要になりますし…。ではStrategyパターンを使ってみましょう。定義にある独立に注目します。

<?php
interface ChargeInterface
{
    public function getName();
    public function getChargeForPeople();
    public function getSum();
}
<?php

class PlanACharge implements ChargeInterface
{
    const CHARGE_PER_PEOPLE = 1000;
    private string $name = 'Plan A';
    private int $numberOfPeople;

    public function __construct(int $numberOfPeople)
    {
        $this->numberOfPeople = $numberOfPeople;
    }

    public function getName() : string
    {
        return $this->name;
    }

    public function getChargeForPeople() : int
    {
        return self::CHARGE_PER_PEOPLE * $this->numberOfPeople;
    }

    public function getSum()
    {
        return $this->getChargeForPeople();
    }
}
<?php

// Bの場合は5人以上だと1割引される
class PlanBCharge implements ChargeInterface
{
    const CHARGE_PER_PEOPLE = 1500;
    private string $name = 'Plan B';
    private int $numberOfPeople;

    public function __construct(int $numberOfPeople)
    {
        $this->numberOfPeople = $numberOfPeople;
    }

    public function getName() : string
    {
        return $this->name;
    }

    public function getChargeForPeople() : int
    {
        return self::CHARGE_PER_PEOPLE * $this->numberOfPeople;
    }

    public function getSum()
    {
        $sum = $this->getChargeForPeople();

        if ($this->numberOfPeople > 4) {
            $sum = $sum * 0.9;
        }

        return $sum;
    }
}
<?php

// Cの場合は利用人数に応じた料金とは別に固定でライセンス料が入る
class PlanCCharge implements ChargeInterface
{
    const CHARGE_PER_PEOPLE = 2000;
    const MONTHLY_LICENCE_CHARGE = 10000;
    private string $name = 'Plan C';
    private int $numberOfPeople;

    public function __construct(int $numberOfPeople)
    {
        $this->numberOfPeople = $numberOfPeople;
    }

    public function getName() : string
    {
        return $this->name;
    }

    public function getChargeForPeople() : int
    {
        return self::CHARGE_PER_PEOPLE * $this->numberOfPeople;
    }

    public function getSum() : int
    {
        return $this->getChargeForPeople() + self::MONTHLY_LICENCE_CHARGE;
    }
}
<?php
require_once('ChargeInterface.php');
require_once('PlanACharge.php');
require_once('PlanBCharge.php');
require_once('PlanCCharge.php');

// 引数はユーザーからの入力を想定した利用人数
$numberOfPeople = 5;

$planA = new PlanACharge($numberOfPeople);
$planB = new PlanBCharge($numberOfPeople);
$planC = new PlanCCharge($numberOfPeople);

echo "<pre>"; print_r('利用人数:'. $numberOfPeople); echo "</pre>";
echo "<pre>"; print_r('プランAの場合の料金:' . $planA->getSum(). '円'); echo "</pre>";
echo "<pre>"; print_r('プランBの場合の料金:' . $planB->getSum(). '円'); echo "</pre>";
echo "<pre>"; print_r('プランCの場合の料金:' . $planC->getSum(). '円'); echo "</pre>";

こう書くとそれぞれのプラン、PlanACharge、PlanBCharge、PlanCChargeが独立しておりカプセル化されています。
どれかのプランの内容に変更があっても互いに影響を及ぼすことがありません。
新しくプランを作った場合でも既存の物に影響を与えません。
これが大きなメリットになります。

定義のそれぞれを交換可能にするという部分については上の例ではインターフェースが一つなのでややわかりにくいですがに新しくHogeインターフェースを作った場合でその見積もりのクラスとHogeをimplementして作成したクラス(HogeA,HogeB, HogeC)は切り替えることができます。
例えばPlanAとHogeAの組み合わせを使ったり、 PlanBとHogeCを使ったりアルゴリズムを切り替えるだけで交換可能になります。
これはHead Firstの動物の例の方がわかりやすいと思うのでよければ読んでみてください。正直なところ↑のコード例のみの場合は抽象クラスの方がわかりやすいですよね…利用例って難しい…

最後にどういうときに使うかについてですが、if文やswitch文の条件分岐が多くなるところで今後変更が予想されるところでは一度StrategyあるいはStateパターンを使うことを検討してみていいと思います。
Stateパターンについては後々書いてみたいと思います。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です