概要

アルゴリズムをオブジェクトとして切り出し、実行時に差し替えられるようにする。Strategy パターンは Java の Comparator インターフェースそのものであり、最も身近なデザインパターンの1つです。ソートアルゴリズムの切り替え、料金計算ロジックの顧客別カスタマイズ、出力形式の動的変更など、同じ処理を異なるやり方で行いたい場面に適用できます。Java 8 以降はラムダ式やメソッド参照で Strategy を簡潔に定義でき、匿名クラスを書く必要がほぼなくなりました。 実務で効果が出やすいのは、if/switch による条件分岐をロジックの外に追い出せることです。Strategy 実装はそれぞれ独立したクラスやラムダ式になるため単体でテストしやすく、既存コードを変更せずに新しいアルゴリズムを追加できます(オープン・クローズドの原則)。Spring などの DI コンテナと組み合わせると、各 Strategy 実装を Bean として登録し、Map<String, T> 注入で設定キーやリクエスト属性から動的に選択する構成が自然に書けます。 この記事では、料金計算(通常 / 割引 / 特別会員)を題材に Context と Strategy の基本構造を示し、ラムダ式による簡略化、Comparator を使った実践例、Java 21 の sealed interface でソート仕様を型安全に定義する方法を確認します。

使いどころ

ソートアルゴリズム(バブル / 選択 / マージ)を実行時に切り替え、データ量に応じた戦略で処理する

顧客種別(一般 / プレミアム / 法人)に応じた料金計算ロジックを Strategy として差し替える

帳票の出力形式(CSV / Excel / PDF)を Strategy で抽象化し、設定に応じた出力を行う

コード例

StrategyPatternDemo.java
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Map;

public class StrategyPatternDemo {

    // ① Strategy インターフェース(関数型にしてラムダ式を許容)
    @FunctionalInterface
    interface PricingStrategy {
        BigDecimal calculate(BigDecimal basePrice);
    }

    // ② 具体的な実装(状態なし → static final で使い回す)
    static final PricingStrategy STANDARD =
        price -> price.setScale(0, RoundingMode.HALF_UP);

    static final PricingStrategy DISCOUNT_10 =
        price -> price.multiply(new BigDecimal("0.9"))
                      .setScale(0, RoundingMode.HALF_UP);

    static final PricingStrategy VIP =
        price -> price.multiply(new BigDecimal("0.8"))
                      .setScale(0, RoundingMode.HALF_UP);

    // ③ 選択ロジックを Map に一元化(if-else を排除)
    static final Map<String, PricingStrategy> STRATEGIES = Map.of(
        "standard",  STANDARD,
        "discount10", DISCOUNT_10,
        "vip",       VIP
    );

    // ④ Context:Strategy を受け取って価格を計算する
    static class PriceCalculator {
        private PricingStrategy strategy;
        PriceCalculator(PricingStrategy s) { this.strategy = s; }
        void setStrategy(PricingStrategy s) { this.strategy = s; }
        BigDecimal calculate(BigDecimal base) {
            return strategy.calculate(base);
        }
    }

    record Person(String name, int age) {}

    public static void main(String[] args) {
        var base = new BigDecimal("10000");
        var calc = new PriceCalculator(STANDARD);

        System.out.println("通常: " + calc.calculate(base));    // 10000

        calc.setStrategy(DISCOUNT_10);
        System.out.println("10%割引: " + calc.calculate(base)); // 9000

        calc.setStrategy(VIP);
        System.out.println("VIP: " + calc.calculate(base));    // 8000

        // Map から動的に選択(if-else なし)
        String customerType = "discount10";
        var selected = STRATEGIES.getOrDefault(customerType, STANDARD);
        System.out.println("動的選択: " + selected.calculate(base)); // 9000

        // Comparator は Strategy パターンそのもの
        var persons = new ArrayList<Person>();
        persons.add(new Person("田中", 35));
        persons.add(new Person("佐藤", 28));
        persons.add(new Person("鈴木", 42));

        persons.sort(Comparator.comparingInt(Person::age));
        System.out.println("年齢順: " + persons);

        persons.sort(Comparator.comparing(Person::name).reversed());
        System.out.println("名前逆順: " + persons);
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

var による型推論と record でデータクラスを簡潔に定義でき、Strategy との組み合わせが書きやすくなった。

Java 17
// Java 17: record + Comparator メソッド参照
record Person(String name, int age) {}
var persons = new ArrayList<Person>();
// ...
var byAge = new ArrayList<>(persons);
byAge.sort(Comparator.comparingInt(Person::age));

Library Comparison

標準 API(interface + ラムダ式)アルゴリズムの種類が少なく、ラムダ式で簡潔に定義できる場面。外部依存なしで完結する。アルゴリズムの組み合わせや設定が複雑になると管理コードが増える。状態なし実装は static final に切り出すと使い回しやすい。
Comparator チェーンソートの多軸条件を thenComparing で連結する場合。標準 API で宣言的に書ける。ソート以外の戦略切り替えには適用できない。
Function / BiFunction汎用的な関数型インターフェースで戦略を受け渡す場合。専用の Strategy interface を定義するほどでもない場面。引数・戻り値の意味が型から読み取りにくく、専用 interface の方がドキュメント性が高い。
Spring DI(Map<String, T> 注入)Spring Boot 環境で Strategy 実装クラスを Bean として管理し、リクエスト属性や設定値をキーに動的に選択したい場合。Spring 依存になるが、@Autowired で Map<String, PricingStrategy> を受け取ると Bean 名から実装を get するだけで選択が完結し、新しい Strategy を追加しても注入先のコードを変更せずに済む。

注意点

Strategy インターフェースのメソッドが1つだけなら関数型インターフェース(@FunctionalInterface)にすることでラムダ式で簡潔に記述できる

Strategy の切り替えが頻繁に発生する場合、Context の setStrategy を外部から呼ぶ設計だとスレッドセーフティの考慮が必要になる

Strategy パターンと State パターンは構造が似ている。Strategy は外部からの明示的な切り替え、State は内部状態に応じた自動切り替えが基本

ラムダ式で Strategy を定義すると簡潔だが、複雑なロジックはクラスとして分離した方がテストしやすい

Strategy を選択する条件分岐(if-else や switch)が Context の内部に残ると Strategy 導入の目的が半減する。選択ロジックは Factory メソッドや Map<String, Strategy> に一元化し、新しい Strategy の追加が既存コードの変更を伴わない構造にする

状態を持たない Strategy 実装はインスタンスを使い回せるためスレッドセーフになる。static final 定数や enum の値として保持しておくと都度 new するコストも避けられる。逆に状態を持つ場合はスコープ(リクエストごと・スレッドごと)を明確にする

FAQ

Strategy パターンと Template Method パターンの違いは何ですか。

Strategy はアルゴリズム全体をオブジェクトとして差し替えます。Template Method は処理の骨格を親クラスに固定し、ステップの一部をサブクラスでカスタマイズします。委譲か継承かの違いです。

ラムダ式で定義した Strategy をテストするにはどうすればよいですか。

ラムダ式を static final 定数やメソッドに抽出し、そのフィールド・メソッドに対してテストを書きます。匿名ラムダのままでは呼び出し元のテストを通じてしか検証できません。

Spring DI 環境で複数の Strategy 実装を動的に切り替えるにはどうしますか。

各実装に @Component を付けて登録し、@Autowired で Map<String, PricingStrategy> を受け取ると Bean 名をキーに選択できます。Map.get(customerType) だけで選択が完結し、新しい Strategy を追加しても注入先のコードを変更する必要がありません。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます