送料計算のように、交換できる複数の処理を共通の型で扱う設計がStrategyです。各処理が「入力を受け取って結果を返す1つの操作」なら、専用の実装クラスをラムダに置き換えられます。共通の型と、それを利用するクラスはそのままです。
一方、設定から名前で選ぶ、選択肢を一覧表示する、戦略ごとに状態を持つ、といった要件があるなら、enum や実装クラスのほうが適しています。この記事の題材は送料計算です。コードを動かして境界を確かめます。
1. Strategyは処理を交換できるようにするパターン
Strategyは、同じ目的を持つ複数の処理を共通の型で包み、利用側から交換できるようにするデザインパターンです。たとえば送料計算に通常便・速達便・大口割引がある場合、if で配送方法を判定する代わりに、計算方法そのものを渡します。
古典的なStrategyの構成要素は3つです。
| 役割 | 責務 |
|---|---|
| Strategy | 利用側が呼び出す共通の操作を定義する |
| ConcreteStrategy | 通常便や速達便など、個々の処理を実装する |
| Context | Strategyを受け取り、どの実装かを意識せずに利用する |
この分離の目的は、クラスを増やすことではありません。Context から処理ごとの分岐を外し、処理を追加・交換しても利用側を変えずに済む形を作ることです。この記事では、送料計算の実装クラスをラムダにしても、その分離を保てる範囲を扱います。
2. ラムダ・enum・実装クラスを先に選び分ける
判断の起点は、処理の短さではなく、戦略をどのように扱うかです。
| 選択 | 向いている条件 |
|---|---|
| ラムダ | 抽象メソッドが1つで、呼び出し側の近くに置ける。安定した名前や内部状態が要らない |
enum | 選択肢が固定され、設定値や画面から名前で選びたい |
| 実装クラス | 複数の操作、依存オブジェクト、可変状態、外部からの拡張が要る |
たとえば「通常配送」と「速達」をコード内で渡すだけならラムダで十分です。「STANDARD」という識別子を保存して後から復元するなら enum、配送会社のAPIクライアントを保持するなら実装クラスが候補になります。
3. 従来のStrategyから消えるのは実装クラスだけ
Windows のターミナルから WSL2 に入り、作業ディレクトリを作成してください。
wsl
mkdir -p ~/projects/java-strategy-pattern-demo
cd ~/projects/java-strategy-pattern-demo
code .
StrategyDemo.java を作成してください。
public class StrategyDemo {
public static void main(String[] args) {
Checkout checkout = new Checkout();
ShippingPolicy standard = new StandardShipping();
ShippingPolicy express = grams -> 1_200;
System.out.println("standard: " + checkout.total(3_000, 800, standard));
System.out.println("express: " + checkout.total(3_000, 800, express));
}
}
@FunctionalInterface
interface ShippingPolicy {
int fee(int grams);
}
final class StandardShipping implements ShippingPolicy {
@Override
public int fee(int grams) {
return grams <= 1_000 ? 500 : 800;
}
}
final class Checkout {
int total(int price, int grams, ShippingPolicy policy) {
return price + policy.fee(grams);
}
}
実行には JDK 25 のコンテナを使います。
docker run --rm -v "$(pwd):/app" -w /app eclipse-temurin:25-jdk java StrategyDemo.java
standard: 3500
express: 4200
対応関係は、ShippingPolicy が Strategy、StandardShipping と express が ConcreteStrategy、Checkout が Context です。ラムダにした express だけは専用クラスを持ちません。
@FunctionalInterface は「抽象メソッドを1つだけ持つ」という設計をコンパイラに検査させる注釈です。ラムダ自体は Java 8 で正式導入されており、この置き換えは JDK 25 固有ではありません。背景仕様は JEP 126: Lambda Expressions & Virtual Extension Methods です。
4. 抽象メソッドが2つになるとラムダは使えない
戦略に表示名まで持たせるため、label() を追加してみます。TwoMethods.java を作成してください。
public class TwoMethods {
public static void main(String[] args) {
ShippingPolicy policy = grams -> 500;
System.out.println(policy.fee(800));
}
}
interface ShippingPolicy {
int fee(int grams);
String label();
}
docker run --rm -v "$(pwd):/app" -w /app eclipse-temurin:25-jdk java TwoMethods.java
TwoMethods.java:3: error: incompatible types: ShippingPolicy is not a functional interface
ShippingPolicy policy = grams -> 500;
^
multiple non-overriding abstract methods found in interface ShippingPolicy
1 error
error: compilation failed
ラムダが実装できるのは、抽象メソッドが1つの関数型インタフェースだけです。表示名が全戦略の責務なら、実装クラスか enum に戻す判断です。表示名がUIだけの都合なら、ShippingPolicy に混ぜず、選択肢を表す別の型に持たせられます。
@FunctionalInterface を付けておけば、2つ目の抽象メソッドを追加した場所で設計変更を検出できます。エラーをラムダの代入箇所まで持ち越さないためにも、ラムダで使う意図があるインタフェースには付けておくと安全です。
5. 名前・一覧・状態が必要なら型を残す
ラムダは処理を渡すには便利ですが、業務上の選択肢そのものではありません。次の要件が出たら、専用の型へ戻す合図です。
- 設定ファイルの
STANDARDから戦略を復元する - 画面に利用可能な配送方法を一覧表示する
- 戦略ごとに説明、上限額、外部サービスの識別子を持つ
- APIクライアントやキャッシュを保持する
- 別モジュールから新しい戦略を追加する
選択肢がアプリケーション内で固定されているなら、enum で名前と実装をまとめる選択肢があります。
enum ShippingOption implements ShippingPolicy {
STANDARD {
public int fee(int grams) {
return grams <= 1_000 ? 500 : 800;
}
},
EXPRESS {
public int fee(int grams) {
return 1_200;
}
}
}
ShippingOption.valueOf("STANDARD") で名前から選べ、ShippingOption.values() で一覧を得られるのが利点です。選択肢を外部モジュールが増やすなら、閉じた一覧である enum ではなく実装クラスとDIコンテナが候補です。
逆に、戦略が1つの計算だけで完結し、呼び出し側が処理を直接選べるなら、実装クラスを増やす理由はありません。ラムダ自体を識別子として保存せず、必要なら識別子と処理を分けてください。
6. Strategyをラムダへ置き換える条件
置き換えてよい条件は次の3つです。
- インタフェースの抽象メソッドが1つである
- 戦略を安定した名前で保存・照合・列挙しない
- 戦略が独立した状態や依存オブジェクトを持たない
3つを満たすなら、ConcreteStrategy をラムダへ寄せても Strategy の役割はそのままです。どれかを満たさないなら、短いクラスにも残す意味があります。
次の記事は、型の一覧を閉じて分岐漏れを検出する Javaのデザインパターン「State」とsealed・switchを使い分ける です。
7. 片付けと、対処
残るものは、作業ディレクトリと Docker イメージです。コンテナには --rm を付けているため、停止後のコンテナは残りません。
削除する前に対象を確認します。
ls -d ~/projects/java-strategy-pattern-demo
docker image ls eclipse-temurin
表示されたディレクトリがこの記事の作業用であり、JDK 25 のイメージをほかの作業で使わない場合だけ削除してください。続けて同シリーズの記事を試す場合、イメージの削除は不要です。
cd ~
rm -rf ~/projects/java-strategy-pattern-demo
docker image rm eclipse-temurin:25-jdk
ShippingPolicy is not a functional interface が出た場合は、ShippingPolicy の抽象メソッドが1つかを確認してください。表示名や初期化処理まで同じインタフェースへ追加しているなら、ラムダへ寄せず enum または実装クラスを選びます。