#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パターンで、イベントと通知を疎結合にする方法を解説します。