🦆

【Laravel】Strategyパターンを使ってみる

2022/11/22に公開

はじめに

今回はデザインパターンの一つであるStrategyパターンについてです。
LaravelのCommandを用いて、電子決済を想定して実装していきます。

環境

参考

https://www.oreilly.co.jp/books/9784873119762/
https://refactoring.guru/ja/design-patterns/strategy
https://www.techscore.com/tech/DesignPattern/Strategy

Strategyパターンとは

ひとことで言えば、状況に応じてアルゴリズムを簡単に切り替えることを可能にするデザインパターンです。
きちんとした定義は下記をご覧ください。

Strategy (ストラテジー、 戦略) は、 振る舞いに関するデザインパターンの一つで、 アルゴリズムのファミリーを定義し、 それぞれのアルゴリズムを別個のクラスとし、 それらのオブジェクトを交換可能にします。

普通にプログラミングしていると、メソッドの中に溶け込んだ形でアルゴリズムを実装してしまうことがよくあります。if 文などで分岐させることでアルゴリズムを変更するような方法です。Strategy パターンでは、戦略の部分を意識して別クラスとして作成するようにしています。戦略x部分を別クラスとして作成しておき、戦略を変更したい場合には、利用する戦略クラスを変更するという方法で対応します。Strategy パターンを利用することで、メソッドの中に溶け込んだ形のアルゴリズムより柔軟でメンテナンスしやすい設計となります。

Strategy
アルゴリズムを利用するためのインターフェースです。
Concrete Strategies
Strategyインターフェースを実際に実装する具象クラス群で、それぞれのアルゴリズムごとにクラスを作成し、実装します。
Context
Concrete Strategies(具象クラス)のインスタンスを持っていて、それを利用します。(呼び出しているのはStrategyインターフェース)

今回の実装について

  • 電子決済を想定
  • 決済方法はApple Pay、Google Pay、クレジットカードの3つ
  • コマンドは$ pay {paymentMethod}
  • 入力された値に応じて決済方法と利用上限を出力する

Strategyパターンを利用しない場合

では、まずStrategyパターンを使用せずに実装してみます。

PaymentStrategyCommand.php
class PaymentStrategyCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'pay {paymentMethod}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '使用される決済方法と利用上限を表示します。';

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        $argument = $this->argument('paymentMethod');

        $this->info(Self::getPaymentMethod($argument));
        $this->info('利用上限:' . Self::getUsageLimit($argument));

        return Command::SUCCESS;
    }

    private static function getPaymentMethod(string $commandArgument) : string
    {
        $paymentMethod = match($commandArgument) {
            'credit' => 'クレジットカード',
            'apple' => 'Apple Pay',
            'google' => 'Google Pay',
            default => throw new \InvalidArgumentException('credit, apple, googleのいずれかを入力してください。'),
        };

        return $paymentMethod;
    }

    private static function getUsageLimit(string $commandArgument) : string
    {
        $usageLimit = match ($commandArgument) {
            'credit' => '¥1,000,000',
            'apple' => '¥100,000',
            'google' => '¥50,000',
            default => throw new \InvalidArgumentException('credit, apple, googleのいずれかを入力してください。'),
        };

        return $usageLimit;
    }
}

match関数で分岐していますが、このような書き方だと決済方法を増やしたり、処理を追加する場合には毎度分岐を書き足さなければいけません。
実際には決済方法はもっとたくさんあるので、さらに分岐が増えていくことは大いに考えられると思います。

Strategyパターンを利用する場合

では、Strategyパターンを利用して実装していきます。

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

PaymentMethodという名前でインターフェースを作成します。

PaymentMethod.php
<?php

namespace App\PaymentMethod;

interface PaymentMethod
{
    public function getPaymentMethod(): string;

    public function getUsageLimit(): string;
}

②具象クラスを作成

クレジットカード、ApplePay、GooglePayそれぞれの種別ごとにクラスを作成します。
これらは先ほどのPaymentMethodの具象クラスになります。

CreditCardStrategy.php
<?php

namespace App\PaymentMethod;

class CreditCardStrategy implements PaymentMethod
{
    public function getPaymentMethod(): string
    {
        return 'クレジットカード';
    }

    public function getUsageLimit(): string
    {
        return '¥1000,000';
    }
}
ApplePayStrategy.php
<?php

namespace App\PaymentMethod;

class ApplePayStrategy implements PaymentMethod
{
    public function getPaymentMethod(): string
    {
        return 'Apple pay';
    }

    public function getUsageLimit(): string
    {
        return '¥50,000';
    }
}
GooglePayStrategy.php
<?php

namespace App\PaymentMethod;

class GooglePayStrategy implements PaymentMethod
{
    public function getPaymentMethod(): string
    {
        return 'Google pay';
    }

    public function getUsageLimit(): string
    {
        return '¥100,000';
    }
}

③具象クラスを利用するためのコンテキストを作成

コマンドで入力される値をコンストラクタの引数とし、その値によって対応するインスタンスを生成します。

PaymentStrategyContext.php
<?php

namespace App\PaymentMethod;

class PaymentStrategyContext
{
    private PaymentMethod $strategy;

    public function __construct(string $paymentMethod)
    {
        $this->strategy = match ($paymentMethod) {
            'credit' => new CreditCardStrategy(),
            'apple' => new ApplePayStrategy(),
            'google' => new GooglePayStrategy(),
            default => throw new \InvalidArgumentException('credit, apple, googleのいずれかを入力してください。'),
        };
    }

    public function getPaymentMethod()
    {
        return $this->strategy->getPaymentMethod();
    }

    public function getUsageLimit()
    {
        return $this->strategy->getUsageLimit();
    }
}

④コマンドを作成

コマンドでは、入力された値を先程のコンテキストに渡してあげるだけで済みます。

PaymentStrategyCommand.php
<?php

namespace App\Console\Commands;

use App\PaymentMethod\PaymentStrategyContext;
use Illuminate\Console\Command;

class PaymentStrategyCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'pay {paymentMethod}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = '使用される決済方法と利用上限を表示します。';

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        $strategyContext = new PaymentStrategyContext($this->argument('paymentMethod'));

        $this->info($strategyContext->getPaymentMethod());
        $this->info('利用上限:' . $strategyContext->getUsageLimit());

        return Command::SUCCESS;
    }
}

決済方法を追加したい場合には、PaymentMethodインターフェスを実装するクラスを追加すれば済むようになりました。
このように、Strategyパターンを利用することで決済方法の追加が容易になり、メンテナンス性を向上させることができます。

さいごに

簡易的ではありますが、今回はデザインパターンの一つであるStrategyパターンについて書かせていただきました。
少しでもお役に立てれば幸いです。
種別ごとに異なるアルゴリズムを実装する必要がある場面は頻出だと思いますので、活用してメンテナンス性高く実装していきたいです。
最後までお読みいただきありがとうございました。

Discussion