Strategyパターンとは?PHP・Java・TypeScriptの実装例と使いどころ
Strategy(ストラテジー)パターンは、処理のアルゴリズムをオブジェクトとして切り出し、実行時に差し替えられるようにするGoFの振る舞いに関するデザインパターンです。本記事ではPHPでの実装を軸に、interface・enum・クロージャの3通りの書き方を示し、Java・TypeScript・Goのコード、StateやTemplate Methodとの違い、そして「使うべき場面・避けるべき場面」の判断基準まで整理します。
まとめ:Strategyパターンの要点
Strategyパターンは、同じ目的(決済・通知・並び替えなど)を達成する手段が複数あるとき、その手段(アルゴリズム)を共通インタフェースの背後に隠し、ifやmatchの分岐を増やさずに実行時に切り替えるためのパターンです。中心となる登場人物はStrategy(戦略インタフェース)・ConcreteStrategy(具体戦略)・Context(利用側)の3つで、Contextは具体的な処理内容を知らないまま戦略に委譲します。
PHPでは、基本形はinterfaceと具体クラスの組み合わせ、固定された選択肢ならPHP 8.1のenumがinterfaceを実装する形、ワンライナーの軽量な切り替えならクロージャ(callable)が向きます。戦略の種類が今後増える見込みがあり実行時に切り替えたいなら有効ですが、分岐が2〜3個で固定なら単純なmatch式のほうが読みやすく、無理にクラス化すると過剰設計になります。以降でPHPの3通りの実装と、他言語・他パターンとの違いを具体的に見ていきます。
Strategyパターンとは:アルゴリズムを戦略として分離する振る舞いパターン
Strategyパターンの定義は「アルゴリズム(戦略)をクラスやオブジェクトとして分離し、実行時に交換できるようにする」ことです。処理の手段を抽象インタフェースで宣言し、手段ごとにそれを実装した具体戦略を用意し、利用側(Context)が実行時に適切な戦略を選んで呼び出します。GoFのデザインパターン23種のうち、オブジェクトの振る舞い(アルゴリズム)を扱う「振る舞いに関するパターン」に分類されます。
解決したいのは、手段を直接if-elseやswitchで分岐させる実装が抱える問題です。決済手段や通知チャネルが増えるたびに分岐を書き足すと、そのメソッドが肥大化し、追加のたびに既存コードへ手を入れることになります。これは「拡張には開き、修正には閉じる」オープン・クローズドの原則(OCP)に反し、変更の影響範囲が読めなくなります。戦略を別クラスに切り出せば、新しい手段はクラスを1つ足すだけで済み、既存コードを触らずに拡張できます。1クラスが1つの手段だけを担うため単一責任の原則(SRP)も守りやすく、戦略ごとに単体テストを書ける利点もあります。
Strategyパターンの構造:Strategy・ConcreteStrategy・Context
Strategyパターンは3つの役割で構成されます。Strategy(戦略インタフェース)は、すべての戦略に共通するメソッド(例:pay())のシグネチャだけを宣言します。Contextはこのインタフェースにのみ依存し、中身の実装を知りません。ConcreteStrategy(具体戦略)は、そのインタフェースを実装した個々のアルゴリズムです。現金決済・カード決済のように、手段ごとに1クラスを用意します。
Context(利用側)は、Strategy型のフィールドを保持し、実処理を戦略へ委譲します。戦略を差し替えるセッターを持たせれば、実行中に振る舞いを動的に切り替えられます。Contextから見れば呼び出しは常に同じメソッド1本で、どの具体戦略が入っているかを意識しません。この「呼び出し口の共通化」と「中身の交換可能性」がStrategyパターンの核です。
PHPでのStrategyパターン実装:interface・enum・クロージャの3通り
PHPはinterfaceを言語機能として持ち、PHP 8.0以降のコンストラクタプロモーションやPHP 8.1のenum・first-class callableによって、Strategyパターンを目的に応じて書き分けられます。ここでは3通りを、それぞれ異なる題材で示します。
interfaceと具体クラスによる基本実装(決済戦略)
もっとも標準的なのが、戦略インタフェースを定義し、決済手段ごとに実装クラスを用意する形です。PHP 8のコンストラクタプロモーション(private string $holder)で、戦略が持つ状態も簡潔に書けます。
<?php
declare(strict_types=1);
// 戦略インタフェース
interface PaymentStrategy {
public function pay(int $amount): string;
}
// 具体戦略
class CreditCardPayment implements PaymentStrategy {
public function __construct(private string $holder) {}
public function pay(int $amount): string {
return "{$amount}円をカード({$this->holder})で決済";
}
}
class CarrierPayment implements PaymentStrategy {
public function pay(int $amount): string {
return "{$amount}円をキャリア決済";
}
}
// コンテキスト
class Checkout {
public function __construct(private PaymentStrategy $strategy) {}
public function setStrategy(PaymentStrategy $strategy): void {
$this->strategy = $strategy;
}
public function process(int $amount): string {
return $this->strategy->pay($amount);
}
}
$checkout = new Checkout(new CreditCardPayment('YAMADA'));
echo $checkout->process(3000), PHP_EOL; // カードで決済
$checkout->setStrategy(new CarrierPayment());
echo $checkout->process(500), PHP_EOL; // キャリア決済
新しい決済手段(例:ポイント払い)を足したいときは、PaymentStrategyを実装したクラスを1つ追加してsetStrategy()で渡すだけで、Checkout本体は変更不要です。実運用ではこの戦略を手書きでnewせず、Laravelのサービスコンテナなどで依存性の注入(DI)を使い、リクエストや設定値に応じた戦略を注入するのが一般的です。戦略の生成ロジックが複雑なら、生成側をファクトリパターンに切り出すと、選択と生成の責務を分離できます。
PHP 8.1のenumで戦略を表現する(会員ランク割引)
戦略の集合が「会員ランク」「配送方法」のように有限で固定なら、PHP 8.1のenumが有力です。enumはinterfaceを実装でき、メソッドを持てるため、ケースごとの振る舞いをmatchで定義した「型安全な戦略」になります。戦略を列挙型のケースとして型で縛れるため、文字列の取り違えをIDEや静的解析(PHPStanなど)で実行前に検出しやすいのが、クラス版にない利点です。
<?php
// 割引戦略をenumで表現(PHP 8.1以降)
interface DiscountStrategy {
public function apply(int $price): int;
}
enum MemberRank: string implements DiscountStrategy {
case Regular = 'regular';
case Gold = 'gold';
case Platinum = 'platinum';
public function apply(int $price): int {
return match($this) {
MemberRank::Regular => $price,
MemberRank::Gold => (int)($price * 0.9),
MemberRank::Platinum => (int)($price * 0.8),
};
}
}
$rank = MemberRank::from('gold');
echo $rank->apply(10000), PHP_EOL; // 9000
ランクを1つ増やすときはcaseとmatchのアームを1行ずつ足します。matchは網羅性が問われるため、追加漏れがあればUnhandledMatchErrorで気づけます。逆に、選択肢が増減する・戦略ごとに大きな依存を注入したい場合はクラス版のほうが柔軟です。enum版は「短く固定された振る舞いの束」に向く、と割り切るのが判断の軸になります。
クロージャ・callableによる軽量Strategy(ソート比較)
戦略が1〜数行で、専用クラスを作るまでもない場合は、クロージャ(callable)をそのまま戦略として渡せます。並び替えの比較関数を差し替えるusort()は、標準関数がStrategyを引数に取る典型例です。アロー関数(fn()、PHP 7.4+)で比較戦略を簡潔に定義できます。
<?php
// クロージャを戦略として渡す(ソート比較の切り替え)
$products = [
['name' => 'A', 'price' => 300],
['name' => 'B', 'price' => 100],
['name' => 'C', 'price' => 200],
];
$byPriceAsc = fn(array $a, array $b): int => $a['price'] <=> $b['price'];
$byNameDesc = fn(array $a, array $b): int => $b['name'] <=> $a['name'];
function sortProducts(array $items, callable $strategy): array {
usort($items, $strategy);
return $items;
}
$cheapest = sortProducts($products, $byPriceAsc);
// 第2引数の戦略を差し替えるだけで並び順を変えられる
$reversed = sortProducts($products, $byNameDesc);
クラスの定義・生成の手間がなく、戦略をその場で組み立てられるのが利点です。PHP 8.1のfirst-class callable記法(strlen(...)のように既存関数をClosure化する書き方)を使えば、名前付き関数をそのまま戦略として渡せます。ただし戦略が状態を持つ・複数メソッドに分かれる場合は、クロージャでは表現しづらくなるため、interface版へ切り替えます。
Java・TypeScript・Goでの実装例
PHP以外の言語でも、共通インタフェースと委譲という骨格は変わりません。それぞれの言語らしい書き方を凝縮して示します。
Java:インタフェースと多態性で切り替える
Javaはインタフェースとポリモーフィズムでそのまま表現できます。ContextはPaymentStrategyだけを見て動作します。
interface PaymentStrategy {
void pay(int amount);
}
class CashPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println(amount + "円を現金で支払い");
}
}
class CreditCardPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println(amount + "円をカードで支払い");
}
}
class PaymentService {
private PaymentStrategy strategy;
public void setStrategy(PaymentStrategy s) { this.strategy = s; }
public void execute(int amount) { strategy.pay(amount); }
}
class Main {
public static void main(String[] args) {
PaymentService service = new PaymentService();
service.setStrategy(new CashPayment());
service.execute(1000);
service.setStrategy(new CreditCardPayment());
service.execute(2000);
}
}
新しい支払い手段はPaymentStrategy実装クラスを追加するだけで対応でき、PaymentServiceは無変更です。関数型インタフェース化してラムダを渡す書き方も可能です。
TypeScript:クラスと関数のどちらでも表現できる
TypeScriptはインタフェースとクラスでJavaと同様に書けます。通知チャネルの切り替えを例にします。
interface Notifier {
notify(message: string): void;
}
class EmailNotifier implements Notifier {
notify(message: string): void {
console.log('メール送信: ' + message);
}
}
class SmsNotifier implements Notifier {
notify(message: string): void {
console.log('SMS送信: ' + message);
}
}
class NotificationService {
constructor(private notifier: Notifier) {}
setNotifier(n: Notifier): void { this.notifier = n; }
send(msg: string): void { this.notifier.notify(msg); }
}
const service = new NotificationService(new EmailNotifier());
service.send('確認コード:1234');
service.setNotifier(new SmsNotifier());
service.send('再送:5678');
TypeScriptは関数も第一級のため、constructor(private notify: (m: string) => void)のように戦略を関数で受け取る、より軽量な書き方も選べます。PHPのクロージャ版と同じ発想です。
Go:インタフェースと関数値で表現する
Goは継承を持ちませんが、インタフェースで戦略を表現できます。状態を更新するメソッドの受信側はポインタレシーバにする点に注意します(値レシーバだとフィールドへの代入が呼び出し元に反映されません)。
package main
import "fmt"
type Operation interface {
Execute(a, b int) int
}
type Add struct{}
func (Add) Execute(a, b int) int { return a + b }
type Multiply struct{}
func (Multiply) Execute(a, b int) int { return a * b }
type Calculator struct{ op Operation }
func (c *Calculator) SetOperation(op Operation) { c.op = op }
func (c *Calculator) Calculate(a, b int) int { return c.op.Execute(a, b) }
func main() {
calc := &Calculator{}
calc.SetOperation(Add{})
fmt.Println(calc.Calculate(3, 4)) // 7
calc.SetOperation(Multiply{})
fmt.Println(calc.Calculate(3, 4)) // 12
}
Goではtype SortFunc func([]int)のように関数型を定義し、関数値そのものを戦略として差し替える書き方も自然で、こちらはPHPやTypeScriptのクロージャ版に対応します。
他パターンとの違い:State・Template Methodとの比較
Strategyパターンは構造が似た他パターンと混同されがちです。特にStateパターンとはクラス図がほぼ同一ですが、目的が異なります。要点を先に表で示します。
| パターン | 差し替える単位 | 切り替えの主体 | 主な用途 |
|---|---|---|---|
| Strategy | アルゴリズム全体 | 外部(利用側) | 手段の使い分け |
| State | 状態ごとの振る舞い | オブジェクト自身 | 状態遷移の表現 |
| Template Method | 手順の一部 | 継承したサブクラス | 骨格の共通化 |
Stateパターンとの違い
Stateパターンは、オブジェクトの内部状態に応じて振る舞いを変え、状態自身が次の状態への遷移を管理します。切り替えはオブジェクトの内部で自律的に起こり、利用側は遷移を意識しません。対してStrategyは、どの戦略を使うかを外部(利用側やDI)が決め、戦略が勝手に別の戦略へ切り替わることはありません。「状態が変わると振る舞いが変わる」のがState、「手段を選んで振る舞いを決める」のがStrategy、と目的で切り分けます。
Template Methodパターンとの違い
Template Methodは、抽象クラスに処理の骨組みを固定し、可変部分だけを抽象メソッドとしてサブクラスでオーバーライドします。差し替えるのは手順の一部で、実現手段は継承です。Strategyは処理全体を戦略オブジェクトに委譲し、アルゴリズムを丸ごと交換します。手段は委譲(コンポジション)です。骨格を保ちつつ一部だけ変えたいならTemplate Method、手順そのものを取り替えたいならStrategyを選びます。
Strategyパターンを使うべき場面・避けるべき場面
Strategyパターンが効くのは、同じ目的に対して手段が複数あり、かつ今後も増える見込みがあるときです。決済手段・通知チャネル・割引ルール・ソート順・バリデーション・圧縮方式など、「結果は同じで手段だけが違う」箇所が候補になります。実行時に切り替えたい、戦略ごとに単体テストしたい、既存コードを触らずに手段を追加したい、という要件があれば導入する価値があります。
一方で、分岐が2〜3個で今後も増えないなら、Strategyパターンは避けるべきです。PHPならmatch式ひとつで書けるロジックをわざわざインタフェースと複数クラスに分けると、コード量と間接参照が増えるだけで読みにくくなります。「将来増えるかもしれない」という推測だけでクラスを量産するのは典型的な過剰設計です。まずはmatchで書き、手段が3つを超えて分岐が育ち始めた、あるいは戦略ごとに固有の依存やテストが必要になった時点でStrategyパターンへ移す。この順序が、PHPでは実務的に扱いやすい判断基準です。
よくある質問
PHPでStrategyパターンはどう実装しますか?
基本は戦略インタフェースを定義し、手段ごとに実装クラスを作り、利用側(Context)がインタフェース型で保持して委譲します。選択肢が固定ならPHP 8.1のenumがinterfaceを実装する形、1〜数行の軽い戦略ならクロージャ(callable)を渡す形も使えます。
Strategyパターンとenum・match式の使い分けは?
戦略の集合が有限で固定、かつ各戦略が短いなら、PHP 8.1のenum+matchが型安全で簡潔です。戦略の数が増減する、戦略ごとに大きな依存を注入する、個別に差し替えテストするといった要件があるなら、interfaceと具体クラスの構成が向きます。
StateパターンとStrategyパターンの違いは?
クラス構造は似ていますが、Stateは状態遷移をオブジェクト自身が管理して自律的に振る舞いを変え、Strategyは外部が手段を選んで振る舞いを決めます。「状態で変わる」ならState、「手段を選ぶ」ならStrategyです。
Strategyパターンはどんなときに使うべきですか?
同じ目的の手段が複数あり今後も増える、実行時に切り替えたい、戦略ごとにテストや再利用をしたい、という条件がそろうときです。決済手段や割引ルールの切り替えが代表例です。
Strategyパターンのデメリットは?
戦略ごとにクラスが増えて構成が煩雑になること、どの戦略を選ぶかを利用側が理解する必要があること、全戦略が同一インタフェースに縛られることです。分岐が少なく固定ならmatch式のほうが適します。