公開日 2026-08-25

Javaのデザインパターン「Strategy」はラムダでどこまで置き換えられるか

Strategyの実装クラスをラムダへ置き換えられる条件を、送料計算のコードで確認する。ラムダ・enum・実装クラスを、抽象メソッド、名前、一覧、状態の要否から選び分ける。

目次

  1. 1. Strategyは処理を交換できるようにするパターン
  2. 2. ラムダ・enum・実装クラスを先に選び分ける
  3. 3. 従来のStrategyから消えるのは実装クラスだけ
  4. 4. 抽象メソッドが2つになるとラムダは使えない
  5. 5. 名前・一覧・状態が必要なら型を残す
  6. 6. Strategyをラムダへ置き換える条件
  7. 7. 片付けと、対処

送料計算のように、交換できる複数の処理を共通の型で扱う設計がStrategyです。各処理が「入力を受け取って結果を返す1つの操作」なら、専用の実装クラスをラムダに置き換えられます。共通の型と、それを利用するクラスはそのままです。

一方、設定から名前で選ぶ、選択肢を一覧表示する、戦略ごとに状態を持つ、といった要件があるなら、enum や実装クラスのほうが適しています。この記事の題材は送料計算です。コードを動かして境界を確かめます。

1. Strategyは処理を交換できるようにするパターン

Strategyは、同じ目的を持つ複数の処理を共通の型で包み、利用側から交換できるようにするデザインパターンです。たとえば送料計算に通常便・速達便・大口割引がある場合、if で配送方法を判定する代わりに、計算方法そのものを渡します。

古典的なStrategyの構成要素は3つです。

役割責務
Strategy利用側が呼び出す共通の操作を定義する
ConcreteStrategy通常便や速達便など、個々の処理を実装する
ContextStrategyを受け取り、どの実装かを意識せずに利用する

この分離の目的は、クラスを増やすことではありません。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、StandardShippingexpress が 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. インタフェースの抽象メソッドが1つである
  2. 戦略を安定した名前で保存・照合・列挙しない
  3. 戦略が独立した状態や依存オブジェクトを持たない

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 または実装クラスを選びます。

シリーズ 1/3

このシリーズ

デザインパターンを新しいJavaで書き直す

  1. 1. Javaのデザインパターン「Strategy」はラムダでどこまで置き換えられるか 現在の記事
  2. 2. Javaのデザインパターン「State」とsealed・switchを使い分ける
  3. 3. Javaのデザインパターン「Visitor」の二重ディスパッチはいつ外せるか