概要
アルゴリズムをオブジェクトとして切り出し、実行時に差し替えられるようにする。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 で抽象化し、設定に応じた出力を行う
コード例
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);
}
}Version Coverage
var による型推論と record でデータクラスを簡潔に定義でき、Strategy との組み合わせが書きやすくなった。
// 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
注意点
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 は処理の骨格を親クラスに固定し、ステップの一部をサブクラスでカスタマイズします。委譲か継承かの違いです。
ラムダ式を static final 定数やメソッドに抽出し、そのフィールド・メソッドに対してテストを書きます。匿名ラムダのままでは呼び出し元のテストを通じてしか検証できません。
各実装に @Component を付けて登録し、@Autowired で Map<String, PricingStrategy> を受け取ると Bean 名をキーに選択できます。Map.get(customerType) だけで選択が完結し、新しい Strategy を追加しても注入先のコードを変更する必要がありません。