「Strategy」パターンとは?サンプルを踏まえてわかりやすく解説!【Java】

  • LINEで送る
「Strategy」パターンとは?サンプルを踏まえてわかりやすく解説!【Java】

重要度:★★★★★

 本記事ではGoFのデザインパターンのプログラムの振る舞いに関するパターンの一つである「Strategy」パターンを解説します。このパターンを一言で説明するならば、「処理方法(戦略)をオブジェクトとして分離し、実行時に自由に切り替えられるパターン」と言えるでしょう。「Strategy」パターンをサンプルを踏まえて解説します。

Strategyパターンとは

Strategyパターンのイメージ図

 「Strategy」は戦略を意味する単語です。処理方法(戦略)を別のクラスに分離します。この時、一つのインタフェースを実装する形で分離するため、実行時にその処理方法(戦略)を自由に切り替えることが可能になります。

 このパターンは、最終的なゴールは一緒だが、その過程が違うような場合に有効です。日常生活の例で例えると以下のような場合が挙げられます。

  • 支払い方法の切り替え
    • 現金、クレジットカード、電子マネー、QRコードなど
  • 通知手段の切り替え
    • メール、SMS、プッシュ通知、Slack通知など
  • ルート検索アルゴリズムの切り替え
    • 最短距離、最速時間、有料道路回避、徒歩優先など(カーナビ・地図アプリ)
  • 認証方式の変更
    • パスワード認証、指紋認証、顔認証、OTPなど

 これらはすべて、Strategyパターンを使うことで処理の差し替え拡張が簡単になります。

Strategyパターンの構成要素

 Strategyパターンの構成要素は主に3つあり、とてもにシンプルなパターンです。

要素役割
Strategy(戦略インタフェース)・実行される処理(戦略)の共通インタフェースを定義する
・利用側(Context)はこのインタフェースに依存する
ConcreteStrategy(具体戦略)・Strategyインタフェースを実装し、個別のアルゴリズムや処理内容を提供する
Context(利用クラス)・Strategyを使って処理を実行するクラス
・必要に応じてConcreteStrategyを動的に切り替える

Strategyパターンの書き方

 実際に日常生活の例の「支払い方法の切り替え」例でStrategyパターンを実装します。

Strategyを使わない場合

 まずStrategyパターンを利用しない方法で「支払い方法の切り替え」の仕組みを実装するとどのようになるでしょうか。おそらく下記のように利用側で支払い方法と金額を指定して、指定された支払い方法によって処理をif文やswitch文で切り替えることになるかと思います。

public enum PaymentMethod {
    CASH, // 現金
    CREDIT_CARD, // クレジットカード
    QR_CODE // QRコード
}

public class NoStrategyPaymentService {
    public void pay(PaymentMethod method, int amount) {
        if (method == PaymentMethod.CASH) {
            System.out.println("現金で" + amount + "円を支払いました。");
        } else if (method == PaymentMethod.CREDIT_CARD) {
            System.out.println("クレジットカードで" + amount + "円を支払いました。");
        } else if (method == PaymentMethod.QR_CODE) {
            System.out.println("QRコードで" + amount + "円を支払いました。");
        } else {
            System.out.println("未対応の支払い方法です。");
        }
        // 支払い処理を追加するにはif文を追加する必要がある。
    }

    public static void main(String[] args) {
        NoStrategyPaymentService service = new NoStrategyPaymentService();

        service.pay(PaymentMethod.CASH, 1000);
        service.pay(PaymentMethod.CREDIT_CARD, 2000);
        service.pay(PaymentMethod.QR_CODE, 1500);
    }
}

 この実装でも正常に動作します。ではなぜ問題があるのでしょうか。 このコードの問題点を下記の表にまとめます。

問題点説明
OCP(オープン/クローズドの原則)に違反・新しい支払い方法を追加するたびに、pay() メソッドの中身を変更しなければならず、クラスの修正が必要になる
・コードの変更に弱く、バグの温床になる
単一責任の原則(SRP)違反NoStrategyPaymentService クラスが複数の責任(各支払い手段の処理)を持っており、テストや保守が難しくなる
再利用性が低い・各支払い方法の処理が一つのメソッド内にベタ書きされているため、他の場所で使い回すことができない
テストの柔軟性がない・支払い方法ごとに処理を分けてテストすることが困難
・if-else文の中身に直接依存しているため、全ての組み合わせでテストする必要がある
可読性が悪い・支払い方法が増えるごとに if-else の分岐が肥大化し、コードが読みづらくなります。
例外処理が散らかる可能性・すべての処理が一箇所に集中しているため、支払い方法ごとのエラーハンドリングが複雑になる

 Strategyを使う場合

 上記の問題点を解決できるのがStrategyパターンです。では実際にStrategyパターンを利用して「支払い方法の切り替え」の仕組みを実装していきます。

 まずはStrategy(戦略インタフェース)を定義します。

public interface PaymentStrategy {
    void pay(int amount);
}

 「お金を支払う」という動作(ゴール)はどの支払い方法も同じです。そのため、戦略インターフェースでは、全ての支払い方法で共通の動作(pay()メソッド)を定義します。

 次にこのPaymentStrategyを実装して、いくつかの支払い方法の戦略を作ります。

public class CashPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println("現金決済: " + amount + "円");
    }
}

public class CreditCardPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println("クレジットカード決済: " + amount + "円");
    }
}

public class QRCodePayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println("QRコード決済: " + amount + "円");
    }
}

 全てPaymentStrategyを実装しています。本記事ではStrategyパターンの紹介のため、詳細な処理は行わずそれぞれの支払い方法の標準出力を行なっています。本来であれば、ここに支払い方法の具体的な処理を記述します。

 次にこのStrategyを利用する、Contextに該当するクラス(PaymentContext)を作ります。

public class PaymentContext {
    private PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    public void executePayment(int amount) {
        paymentStrategy.pay(amount);
    }
}

 このクラスはPaymentStrategyをフィールドに持っており、このフィールドに入るインスタンスを切り替えていくことで、支払い方法を切り替えることができるようになっています。実際に支払い処理をするのはStrategyインターフェースで定義されているpay()メソッドをexecutePayment()メソッド内で実行します。

 最後に、このPaymentContextとPaymentStrategyを使って支払い方法を切り替えてログを出してみます。

public class StrategySample {
    public static void main(String[] args) {
        
        PaymentContext context = new PaymentContext();
        
        context.setPaymentStrategy(new CreditCardPayment());
        context.executePayment(1000); // クレジットカード決済: 1000円

        context.setPaymentStrategy(new CashPayment());
        context.executePayment(2000); // 現金決済: 2000円

        context.setPaymentStrategy(new QRCodePayment());
        context.executePayment(3000); // QRコード決済: 3000円
    }
}
>> 実行結果
クレジットカード決済: 1000円
現金決済: 2000円
QRコード決済: 3000円

 これでStrategyを利用して、「支払い方法の切り替え」の仕組みを実装することができました。if文やSwitch文を利用していないため非常に拡張しやすい構造となっています。

StrategyパターンとStateパターンの違い

 余談ですが、このStrategyパターンはStateパターンと構造が大変よく似ています。インターフェースを実装したクラスを切り替えることで処理を切り替える部分はほとんど同じです。ただこの2つのパターンには明確な違いがあります。

項目StrategyState
目的処理アルゴリズム(戦略)を柔軟に切り替えるオブジェクトの状態に応じて振る舞いを変える
切り替えの主体利用者(クライアント)が戦略を明示的に設定するオブジェクト自身が状態を内部で自律的に遷移する
使われ方処理方法を選択可能にしたいとき(例:支払い方法、ソートアルゴリズム)状態によって動作を変えたいとき(例:ドアの開閉状態、ゲームキャラのモード)
状態遷移原則、戦略の切り替えは明示的状態オブジェクトが次の状態を内部的に決めて切り替えることが多い
利用例支払い方法の切り替え、ファイル保存形式の選択など文書の編集中/読取専用の切り替え、ATMの状態(待機・入力中・処理中)など

これらの違いから、利用者が処理方法を選びたい(処理手段の切り替え)場合はStrategyパターンを、状態の変化によって動作を自律的に変えたい(内部ロジック)場合はStateパターンを選択するようにしましょう。

 Stateパターンの説明は下記の記事で紹介しています。ぜひ参考にしてみてください。

メリットとデメリット

メリット

メリット説明
処理の切り替えが容易・実行時に処理(戦略)を柔軟に差し替えられる
・条件分岐(if-else)をなくすことができる
拡張が容易(OCP対応)・新しい戦略を追加しても既存のコードを変更せずに済む(オープン/クローズドの原則
処理の再利用が可能・各戦略を独立したクラスとして定義するため、ロジックを使い回しやすく、テストもしやすい
単一責任の原則(SRP)を守れる・各戦略クラスが単一の目的(1つのアルゴリズムや処理)を持つため保守性が高い

デメリット

デメリット説明
クラス数が増える・各戦略ごとに新しいクラスを作成する必要があるため、小規模なプロジェクトでは冗長になることがある
戦略の選択をクライアントに任せる必要があるStrategyを使う側(Contextの利用者)がどの戦略を使うかを知っている必要があり、使い方を誤る可能性がある
すべての戦略が同じインタフェースに従う必要がある強制的に同じメソッドシグネチャになるため、戦略間のインタフェース差異が吸収しづらい場合がある

 Strategyパターンには上記のメリットとデメリットがあります。これらのメリットとデメリットから、Strategyパターンが有効に働くシチュエーションには下記のような場合があります。

  • 同じ目的に対して複数の処理方法(戦略)が存在し、切り替えたいとき
    • 例:支払い方法、通知手段、圧縮方式など
  • 処理ごとの実装を分離・拡張しやすくしたいとき
    • 例:新しい戦略を追加したい、変更を既存コードに影響させたくない
  • 処理ごとにテストや再利用が必要なとき
    • 例:各処理を個別にテストしたい、他の場所でも使い回したい

まとめ

 Strategyパターンは、同じ目的に対して異なる処理方法(戦略)を柔軟に切り替えるための設計手法です。支払い方法や通知手段など、処理の「過程」が複数ある場面に適しています。

 処理をクラスごとに分離することで、拡張性・再利用性・テストのしやすさが向上します。ただし、小規模なケースでは設計が複雑になるため、使いどころを見極めることが大切です。

 本記事で利用したサンプルケースのクラス図は下記になります。

 最後に「Strategy」パターンとは「処理方法(戦略)をオブジェクトとして分離し、実行時に自由に切り替えられるパターン」です。うまく使っていきましょう。

 本記事で利用したコードは下記のgit上で公開しております。

https://github.com/tamotech1028/strategy


デザインパターン講座 目次

第1章 導入

第2章 オブジェクトの「生成」に関するパターン

第3章 プログラムの「構造」に関するパターン

第4章 オブジェクトの「振る舞い」に関するパターン

最新の投稿

SNSでもご購読できます。

コメント

コメントを残す