Strategyパターン|アルゴリズムの動的な切り替え

Strategyパターン|アルゴリズムの動的な切り替え

更新日:2025年10月16日

オブジェクト指向プログラミングにおいて、同じ目的を達成するための複数のアルゴリズムが存在する場合があります。例えば、決済方法(クレジットカード、PayPal、暗号通貨)、ソートアルゴリズム(クイックソート、マージソート)、圧縮方式(ZIP、GZIP)など、状況に応じて最適な方法を選択したい場面は少なくありません。Strategyパターンは、アルゴリズムをカプセル化し、実行時に動的に切り替え可能にすることで、柔軟で保守性の高い設計を実現します。個人的な関心から調査・考察してみましたので、同じように関心をお持ちの方に参考になれば幸いです。

Strategyパターンが解決する問題

問題:if-elseやswitch文の乱立

アルゴリズムの切り替えをif-elseやswitch文で実装すると、以下の問題が発生します。

❌ 悪い設計例:switch文による分岐
public class PaymentProcessor {
    public void processPayment(String paymentType, double amount) {
        switch (paymentType) {
            case "CREDIT_CARD":
                // クレジットカード処理
                System.out.println("クレジットカードで" + amount + "円を決済");
                break;
            case "PAYPAL":
                // PayPal処理
                System.out.println("PayPalで" + amount + "円を決済");
                break;
            case "CRYPTOCURRENCY":
                // 暗号通貨処理
                System.out.println("暗号通貨で" + amount + "円を決済");
                break;
            default:
                throw new IllegalArgumentException("未対応の決済方法");
        }
    }
}
この設計の問題点
1. 開放閉鎖原則違反:新しい決済方法を追加するたびに、既存コードを修正する必要がある
2. 単一責任原則違反:1つのクラスが複数のアルゴリズムを持っている
3. テストの困難さ:各決済方法を個別にテストできない
4. 保守性の低下:コードが長くなり、可読性が低下する

解決策:Strategyパターン

Strategyパターンは、アルゴリズムを個別のクラスとして定義し、それらを交換可能にすることで、上記の問題を解決します。

「アルゴリズムのファミリーを定義し、それぞれをカプセル化して、それらを交換可能にする。Strategyパターンにより、アルゴリズムをそれを使用するクライアントから独立して変更できる。」
— Gang of Four, Design Patterns

パターンの構造

UML図

登場する役割

役割 説明 実装例
Strategy(戦略) アルゴリズムの共通インターフェース PaymentStrategy
ConcreteStrategy(具体的戦略) Strategyインターフェースを実装した具体的なアルゴリズム CreditCardStrategy, PayPalStrategy
Context(文脈) Strategyを保持し、使用するクライアント ShoppingCart

Java実装例

基本実装:決済システム

PaymentStrategy.java(Strategyインターフェース)
/**
 * 決済戦略の共通インターフェース
 */
public interface PaymentStrategy {
    /**
     * 決済を実行する
     * @param amount 決済金額
     */
    void pay(double amount);
}
CreditCardStrategy.java(ConcreteStrategy)
/**
 * クレジットカード決済の実装
 */
public class CreditCardStrategy implements PaymentStrategy {
    private String cardNumber;
    private String name;
    
    public CreditCardStrategy(String cardNumber, String name) {
        this.cardNumber = cardNumber;
        this.name = name;
    }
    
    @Override
    public void pay(double amount) {
        System.out.println(amount + "円をクレジットカードで決済しました");
        System.out.println("カード番号: " + maskCardNumber(cardNumber));
    }
    
    private String maskCardNumber(String cardNumber) {
        // 最後の4桁以外をマスク
        return "**** **** **** " + cardNumber.substring(cardNumber.length() - 4);
    }
}
PayPalStrategy.java(ConcreteStrategy)
/**
 * PayPal決済の実装
 */
public class PayPalStrategy implements PaymentStrategy {
    private String email;
    
    public PayPalStrategy(String email) {
        this.email = email;
    }
    
    @Override
    public void pay(double amount) {
        System.out.println(amount + "円をPayPalで決済しました");
        System.out.println("アカウント: " + email);
    }
}
CryptocurrencyStrategy.java(ConcreteStrategy)
/**
 * 暗号通貨決済の実装
 */
public class CryptocurrencyStrategy implements PaymentStrategy {
    private String walletAddress;
    
    public CryptocurrencyStrategy(String walletAddress) {
        this.walletAddress = walletAddress;
    }
    
    @Override
    public void pay(double amount) {
        System.out.println(amount + "円を暗号通貨で決済しました");
        System.out.println("ウォレット: " + walletAddress);
    }
}
ShoppingCart.java(Context)
/**
 * ショッピングカート(コンテキスト)
 */
public class ShoppingCart {
    private PaymentStrategy paymentStrategy;
    
    /**
     * 決済方法を設定する
     */
    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }
    
    /**
     * 商品を購入する
     */
    public void checkout(double amount) {
        if (paymentStrategy == null) {
            throw new IllegalStateException("決済方法が設定されていません");
        }
        paymentStrategy.pay(amount);
    }
}
Main.java(使用例)
public class Main {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();
        
        // クレジットカードで決済
        cart.setPaymentStrategy(
            new CreditCardStrategy("1234567812345678", "山田太郎")
        );
        cart.checkout(5000);
        
        System.out.println("---");
        
        // PayPalで決済
        cart.setPaymentStrategy(
            new PayPalStrategy("yamada@example.com")
        );
        cart.checkout(3000);
        
        System.out.println("---");
        
        // 暗号通貨で決済
        cart.setPaymentStrategy(
            new CryptocurrencyStrategy("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb")
        );
        cart.checkout(10000);
    }
}

実行結果

5000.0円をクレジットカードで決済しました
カード番号: **** **** **** 5678
---
3000.0円をPayPalで決済しました
アカウント: yamada@example.com
---
10000.0円を暗号通貨で決済しました
ウォレット: 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb

Java 8以降:ラムダ式を活用した実装

Java 8のラムダ式を使うと、シンプルなStrategyはクラスを作らずに定義できます。

ラムダ式による実装
public class LambdaStrategyExample {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();
        
        // ラムダ式でStrategyを定義
        cart.setPaymentStrategy(amount -> 
            System.out.println(amount + "円を現金で決済しました")
        );
        cart.checkout(2000);
        
        // メソッド参照も使用可能
        cart.setPaymentStrategy(LambdaStrategyExample::bankTransfer);
        cart.checkout(15000);
    }
    
    private static void bankTransfer(double amount) {
        System.out.println(amount + "円を銀行振込で決済しました");
    }
}

使用シーンとベストプラクティス

適用すべき状況

Strategyパターンが有効なケース

  • 同じ目的の複数のアルゴリズム:ソート、圧縮、暗号化など、目的は同じだが実装方法が異なる
  • if-elseやswitch文の乱立:条件分岐が多く、保守性が低下している
  • 実行時の切り替え:アルゴリズムを動的に変更する必要がある
  • アルゴリズムの独立性:各アルゴリズムを独立してテスト・保守したい

実際の適用例

用途 Strategy例 切り替えタイミング
ソート クイックソート、マージソート、ヒープソート データサイズや特性に応じて
圧縮 ZIP、GZIP、LZ4、Brotli ファイルサイズ、速度要件に応じて
決済 クレジットカード、電子マネー、QR決済 ユーザーの選択に応じて
割引計算 通常会員、プレミアム会員、法人会員 会員種別に応じて
ルート検索 最短距離、最速時間、最安コスト ユーザーの優先順位に応じて

避けるべき状況

Strategyパターンを使わない方が良いケース
1. アルゴリズムが1つしかない:将来的にも増えない場合は不要
2. アルゴリズムが単純:数行で済む処理にパターンを適用すると過剰設計
3. Strategyが頻繁に変わる:大量のStrategyクラスが必要になる場合はラムダ式を検討
4. Strategyが状態を持つ:複雑な状態管理が必要な場合はStateパターンを検討

メリットとデメリット

メリット

Strategyパターンの利点

  • 開放閉鎖原則の実現:新しいアルゴリズムの追加が既存コードの変更なしで可能
  • 単一責任原則の実現:各アルゴリズムが独立したクラスになる
  • テスト容易性:各Strategyを個別にユニットテスト可能
  • 条件分岐の削減:if-elseやswitch文を排除できる
  • 実行時の切り替え:動的にアルゴリズムを変更可能
  • コードの可読性:アルゴリズムの意図が明確になる

デメリット

デメリット 対策
クラス数の増加 Java 8以降はラムダ式で軽減可能
Strategyの選択責任 Factory Methodパターンと組み合わせる
クライアントの知識 Contextが適切なデフォルト値を提供する
通信オーバーヘッド 必要なデータのみを渡す設計にする

他のパターンとの関連

Factory Methodパターンとの組み合わせ

Strategyの選択をFactory Methodに委譲することで、クライアントがStrategyの詳細を知る必要がなくなります。

PaymentStrategyFactory.java
public class PaymentStrategyFactory {
    public static PaymentStrategy createStrategy(String type, String... params) {
        switch (type) {
            case "CREDIT_CARD":
                return new CreditCardStrategy(params[0], params[1]);
            case "PAYPAL":
                return new PayPalStrategy(params[0]);
            case "CRYPTO":
                return new CryptocurrencyStrategy(params[0]);
            default:
                throw new IllegalArgumentException("未対応の決済方法: " + type);
        }
    }
}

// 使用例
ShoppingCart cart = new ShoppingCart();
PaymentStrategy strategy = PaymentStrategyFactory.createStrategy(
    "CREDIT_CARD", "1234567812345678", "山田太郎"
);
cart.setPaymentStrategy(strategy);
cart.checkout(5000);

Stateパターンとの違い

観点 Strategy State
目的 アルゴリズムの交換 状態に応じた振る舞いの変更
切り替え クライアントが明示的に切り替え 状態遷移によって自動的に切り替わる
独立性 各Strategyは独立 各Stateは相互に関連
用途 複数の解決方法から選択 オブジェクトのライフサイクル管理

Template Methodパターンとの違い

Strategy:オブジェクトの合成によりアルゴリズム全体を交換
Template Method:継承によりアルゴリズムの一部を変更
選択の指針
Strategyを選ぶ:実行時に切り替える必要がある、複数のStrategyを組み合わせる可能性がある
Template Methodを選ぶ:アルゴリズムの骨格は固定、細部のみをカスタマイズしたい

まとめ

Strategyパターンは、アルゴリズムの交換可能性を実現する強力なパターンです。開放閉鎖原則を守り、保守性の高いコードを書くために、条件分岐が増えてきたら積極的に適用を検討すべきです。Java 8以降のラムダ式により、さらに簡潔な実装が可能になりました。

実務では、Factory Methodと組み合わせてStrategyの生成を隠蔽したり、Dependency Injectionと組み合わせてテスト容易性を高めたりすることが一般的です。適切な場面でStrategyパターンを活用し、柔軟で拡張性の高いシステムを構築していきましょう。

参考・免責事項
本記事は2025年10月16日時点の情報に基づいて作成されています。記事内容は個人的な考察に基づくものであり、特定のフレームワークやライブラリの最新仕様については公式ドキュメントをご確認ください。実装例はJava 8以降を想定しています。プロダクション環境での適用については、プロジェクトの要件やチームの方針に応じて適切に判断してください。