#05 デザインパターン入門

Strategy——アルゴリズムを差し替え可能にする

問題: 条件分岐がどんどん膨らむ

支払い処理を実装するとき、支払い方法ごとに処理が異なります。最初は「クレジットカードだけ」でも、やがて「コンビニ払い」「銀行振込」「後払い」が追加されます。

// 問題のあるコード: 支払い方法が増えるたびに修正が必要
class PaymentService
{
    public function pay(string $method, int $amount): void
    {
        if ($method === 'credit_card') {
            // クレジットカード処理
            echo "クレジットカードで {$amount}円 を決済します\n";
            // Stripe API を呼ぶ処理...
        } elseif ($method === 'convenience') {
            // コンビニ払い処理
            echo "コンビニ払いで {$amount}円 の番号を発行します\n";
            // 番号発行API を呼ぶ処理...
        } elseif ($method === 'bank_transfer') {
            // 銀行振込処理
            echo "銀行振込で {$amount}円 の振込先を案内します\n";
            // 口座情報を返す処理...
        }
        // 「後払い」を追加するたびにここを修正...
    }
}

このコードは支払い方法が増えるほど肥大化し、テストも難しくなります。


パターン: Strategy

Strategyパターンは、アルゴリズム(処理の方法)をインターフェースの後ろに隠し、実行時に差し替え可能にするパターンです。

+----------------------------------+
|   PaymentStrategy <<interface>>  |
+----------------------------------+
| + pay(amount: int): void         |
+----------------------------------+

     _________|_________
    |          |        |
CreditCard  Convenience  BankTransfer
Strategy    Strategy     Strategy


+----------------------------------+
|       PaymentService             |
+----------------------------------+
| - strategy: PaymentStrategy      |
+----------------------------------+
| + setStrategy(s: PaymentStrategy)|
| + pay(amount: int): void         |
+----------------------------------+

PHP 8.x での実装

<?php

// 戦略インターフェース
interface PaymentStrategy
{
    public function pay(int $amount): void;
    public function getLabel(): string;
}

// クレジットカード戦略
class CreditCardStrategy implements PaymentStrategy
{
    public function __construct(
        private readonly string $cardNumber,
        private readonly string $cvv,
    ) {}

    public function pay(int $amount): void
    {
        echo "クレジットカード({$this->cardNumber}末尾4桁)で {$amount}円 を決済します\n";
        // Stripe / PAY.JP 等のAPI呼び出し
    }

    public function getLabel(): string
    {
        return 'クレジットカード';
    }
}

// コンビニ払い戦略
class ConvenienceStoreStrategy implements PaymentStrategy
{
    public function pay(int $amount): void
    {
        $code = random_int(100000, 999999);
        echo "コンビニ払い: {$amount}円 の支払い番号 {$code} を発行しました\n";
        // 番号発行APIの呼び出し
    }

    public function getLabel(): string
    {
        return 'コンビニ払い';
    }
}

// 銀行振込戦略
class BankTransferStrategy implements PaymentStrategy
{
    public function pay(int $amount): void
    {
        echo "銀行振込: 三井住友銀行 普通口座 1234567 へ {$amount}円 をお振り込みください\n";
    }

    public function getLabel(): string
    {
        return '銀行振込';
    }
}

// コンテキストクラス
class PaymentService
{
    private PaymentStrategy $strategy;

    public function __construct(PaymentStrategy $strategy)
    {
        $this->strategy = $strategy;
    }

    // 実行時に戦略を切り替えられる
    public function setStrategy(PaymentStrategy $strategy): void
    {
        $this->strategy = $strategy;
    }

    public function pay(int $amount): void
    {
        echo "[{$this->strategy->getLabel()}] ";
        $this->strategy->pay($amount);
    }
}

使う側では「どの戦略を使うか」だけを決めます。

// クレジットカードで支払い
$service = new PaymentService(new CreditCardStrategy('4111111111111111', '123'));
$service->pay(5800);

// 途中でコンビニ払いに変更
$service->setStrategy(new ConvenienceStoreStrategy());
$service->pay(5800);

// 新しい支払い方法(後払い)を追加しても PaymentService は変更不要
class DeferredPaymentStrategy implements PaymentStrategy
{
    public function pay(int $amount): void
    {
        echo "後払い: 翌月末までに {$amount}円 をお支払いください\n";
    }

    public function getLabel(): string { return '後払い'; }
}

$service->setStrategy(new DeferredPaymentStrategy());
$service->pay(5800);

ソートアルゴリズムへの応用

Strategyパターンの古典的な例はソートです。

<?php

interface SortStrategy
{
    public function sort(array &$data): void;
}

class BubbleSortStrategy implements SortStrategy
{
    public function sort(array &$data): void
    {
        $n = count($data);
        for ($i = 0; $i < $n - 1; $i++) {
            for ($j = 0; $j < $n - $i - 1; $j++) {
                if ($data[$j] > $data[$j + 1]) {
                    [$data[$j], $data[$j + 1]] = [$data[$j + 1], $data[$j]];
                }
            }
        }
    }
}

class QuickSortStrategy implements SortStrategy
{
    public function sort(array &$data): void
    {
        sort($data); // 簡略化: 実際はquicksortアルゴリズム
    }
}

class Sorter
{
    public function __construct(private SortStrategy $strategy) {}

    public function sort(array $data): array
    {
        $this->strategy->sort($data);
        return $data;
    }
}

$sorter = new Sorter(new QuickSortStrategy());
$result = $sorter->sort([5, 3, 8, 1, 9, 2]);
print_r($result); // [1, 2, 3, 5, 8, 9]

Laravelのカスタムバリデーションへの応用

Laravelのバリデーションルールは、Strategyパターンの考え方で設計されています。

// カスタムバリデーションルール(Strategyとして機能)
use Illuminate\Contracts\Validation\ValidationRule;

class JapanesePhoneNumber implements ValidationRule
{
    public function validate(string $attribute, mixed $value, \Closure $fail): void
    {
        if (!preg_match('/^(0[5-9]0-\d{4}-\d{4}|0\d{1,4}-\d{1,4}-\d{4})$/', $value)) {
            $fail("{$attribute}は日本の電話番号形式で入力してください。");
        }
    }
}

class PostalCode implements ValidationRule
{
    public function validate(string $attribute, mixed $value, \Closure $fail): void
    {
        if (!preg_match('/^\d{3}-\d{4}$/', $value)) {
            $fail("{$attribute}は〒XXX-XXXXの形式で入力してください。");
        }
    }
}

// コントローラで使う
$request->validate([
    'phone'       => ['required', new JapanesePhoneNumber()],
    'postal_code' => ['required', new PostalCode()],
]);

ValidationRule インターフェースを実装することで、バリデーションロジックを差し替え可能なStrategyとして扱っています。


if/else を排除する

Strategyパターンを導入する際、配列マップを使うと if/elseif をすっきり消せます。

// Strategyをマップで管理
class PaymentStrategyFactory
{
    private array $strategies;

    public function __construct(
        private readonly string $creditCardNumber,
    ) {
        $this->strategies = [
            'credit_card'  => fn() => new CreditCardStrategy($this->creditCardNumber, ''),
            'convenience'  => fn() => new ConvenienceStoreStrategy(),
            'bank_transfer' => fn() => new BankTransferStrategy(),
        ];
    }

    public function create(string $method): PaymentStrategy
    {
        if (!isset($this->strategies[$method])) {
            throw new \InvalidArgumentException("未対応の支払い方法: {$method}");
        }

        return ($this->strategies[$method])();
    }
}

// if/elseif がゼロのすっきりしたコード
$factory = new PaymentStrategyFactory('4111111111111111');
$service = new PaymentService($factory->create($request->payment_method));
$service->pay($order->total_amount);

まとめ

  • Strategyパターンはアルゴリズムをインターフェースに閉じ込め、実行時に切り替え可能にする
  • if/elseif による分岐をなくし、新しい戦略を追加しても既存コードを変更しない
  • 支払い処理・ソート・バリデーションなど「方法を選択する」場面に適している
  • LaravelのカスタムバリデーションルールはStrategyパターンの実例

次回はObserverパターンで、イベントと通知を疎結合にする方法を解説します。