java-recipes

ホーム デザインパターン › DP-21

DP-21: Strategy パターン

アルゴリズムをオブジェクト化して実行時に切り替えられるようにするパターンです。 Java の Comparator インターフェースはこのパターンそのものであり、 ラムダ式や関数型インターフェースとも深く結びついています。

Strategy パターンとは

Strategy パターンは「アルゴリズム(処理の方法)をクラスとしてカプセル化し、 互いに交換できるようにする」パターンです。 Context クラス(処理を行うクラス)はアルゴリズムの詳細を知らず、 Strategy インターフェースを通じて処理を委譲するだけです。 これにより Context を変更せずにアルゴリズムだけを差し替えられます。

Comparator は Strategy パターンそのもの

Java の java.util.Comparator は Strategy パターンの典型例です。List.sort() が Context、Comparator が Strategy インターフェースに相当します。 名前順・年齢順・降順など、ソート基準(戦略)を外から注入できるため、List.sort() のコードを一切変更せずに並び順を切り替えられます。

ラムダ式・関数型インターフェースとの親和性

Strategy インターフェースは抽象メソッドが1つだけの「関数型インターフェース」として設計できます。 Java 8 以降はラムダ式や匿名クラスで Strategy を簡潔に記述できるため、 Strategy パターンとラムダ式は非常に相性が良いです。byAge.sort((a, b) -> Integer.compare(a.getAge(), b.getAge()))のように、1行で戦略を定義できます。

サンプルコード

StrategyPatternSample.java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

public class StrategyPatternSample {

    // ソートアルゴリズムを表すインターフェース(Strategy)
    interface SortStrategy {
        // 配列をソートして返す(元の配列は変更しない)
        int[] sort(int[] data);
    }

    // バブルソートによる実装(計算量: O(n²))
    static class BubbleSortStrategy implements SortStrategy {
        @Override
        public int[] sort(int[] data) {
            int[] result = Arrays.copyOf(data, data.length); // 元の配列をコピー
            int n = result.length;
            for (int i = 0; i < n - 1; i++) {
                for (int j = 0; j < n - i - 1; j++) {
                    if (result[j] > result[j + 1]) {
                        int temp = result[j];
                        result[j] = result[j + 1];
                        result[j + 1] = temp;
                    }
                }
            }
            System.out.println("  [バブルソート実行]");
            return result;
        }
    }

    // 選択ソートによる実装(計算量: O(n²))
    static class SelectionSortStrategy implements SortStrategy {
        @Override
        public int[] sort(int[] data) {
            int[] result = Arrays.copyOf(data, data.length);
            int n = result.length;
            for (int i = 0; i < n - 1; i++) {
                int minIdx = i;
                for (int j = i + 1; j < n; j++) {
                    if (result[j] < result[minIdx]) minIdx = j;
                }
                int temp = result[minIdx];
                result[minIdx] = result[i];
                result[i] = temp;
            }
            System.out.println("  [選択ソート実行]");
            return result;
        }
    }

    // Strategy を使う Context クラス
    static class DataProcessor {
        private SortStrategy strategy; // 現在のソート戦略

        public DataProcessor(SortStrategy strategy) {
            this.strategy = strategy;
        }

        // 実行時に戦略を切り替えられるのが Strategy パターンの特徴
        public void setStrategy(SortStrategy strategy) {
            this.strategy = strategy;
        }

        public int[] process(int[] data) {
            return strategy.sort(data);
        }
    }

    // 社員情報クラス(Comparator による Strategy 活用例)
    static class Employee {
        private final String name;
        private final int age;

        public Employee(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() { return name; }
        public int getAge() { return age; }

        @Override
        public String toString() { return name + "(" + age + "歳)"; }
    }

    public static void main(String[] args) {
        System.out.println("=== Strategy パターン: ソートアルゴリズムの切り替え ===");
        System.out.println();

        int[] data = {5, 2, 8, 1, 9, 3};
        System.out.println("ソート前: " + Arrays.toString(data));
        System.out.println();

        // バブルソート戦略で処理開始
        DataProcessor processor = new DataProcessor(new BubbleSortStrategy());
        int[] result1 = processor.process(data);
        System.out.println("バブルソート結果: " + Arrays.toString(result1));

        System.out.println();

        // 実行時に戦略を選択ソートに切り替え
        System.out.println("--- 戦略を選択ソートに切り替え ---");
        processor.setStrategy(new SelectionSortStrategy());
        int[] result2 = processor.process(data);
        System.out.println("選択ソート結果: " + Arrays.toString(result2));

        System.out.println();
        System.out.println("=== Comparator は Strategy パターンそのもの ===");
        System.out.println();

        List<Employee> employees = new ArrayList<Employee>();
        employees.add(new Employee("田中", 35));
        employees.add(new Employee("佐藤", 28));
        employees.add(new Employee("鈴木", 42));
        employees.add(new Employee("山田", 25));

        System.out.println("元のリスト: " + employees);

        // 名前順でソート(戦略1: 匿名クラスで Comparator を定義)
        List<Employee> byName = new ArrayList<Employee>(employees);
        byName.sort(new Comparator<Employee>() {
            @Override
            public int compare(Employee a, Employee b) {
                return a.getName().compareTo(b.getName());
            }
        });
        System.out.println("名前順: " + byName);

        // 年齢順でソート(戦略2: ラムダ式で Comparator を定義)
        List<Employee> byAge = new ArrayList<Employee>(employees);
        byAge.sort((a, b) -> Integer.compare(a.getAge(), b.getAge()));
        System.out.println("年齢順: " + byAge);

        // 年齢降順でソート(戦略3: ラムダ式)
        List<Employee> byAgeDesc = new ArrayList<Employee>(employees);
        byAgeDesc.sort((a, b) -> Integer.compare(b.getAge(), a.getAge()));
        System.out.println("年齢降順: " + byAgeDesc);
    }
}

Java 8 では Comparator をラムダ式で記述できるため、ソート戦略(Strategy)の実装がとても簡潔になります。Comparator 自体が Strategy パターンの実例です。匿名クラスとラムダ式の両方で書けることを確認しましょう。

よくあるミス・注意点

⚠️ if-else でアルゴリズムを分岐させる(Strategy に置き換えで解決)

アルゴリズムを if-else で切り替えるコードは、新しい種類を追加するたびに 既存の分岐処理を修正しなければなりません。 この分岐が複数箇所に散らばると、修正漏れによるバグが起きやすくなります。 Strategy パターンに置き換えることで、新しいアルゴリズムを追加するときは 新しいクラスを1つ追加するだけで済みます。

⚠️ Strategy クラスにフィールド(状態)を持たせて想定外の挙動を引き起こす

Strategy は原則としてステートレス(状態を持たない)に設計しましょう。 インスタンスを複数の Context で共有したとき、フィールドの値が干渉して 予期しない結果になることがあります。 ラムダ式で Strategy を記述すると自然とステートレスになるため、 可能ならラムダ式を使うのが安全です。

⚠️ Comparator.comparing() の戻り値の型を間違える

Comparator.comparing() は文字列や参照型に、Comparator.comparingInt() は int に使います。 int フィールドに comparing() を使うとオートボクシングが発生してパフォーマンスが落ちます。 int / long / double には専用の comparingInt/Long/Double を使いましょう。

テストする観点

  • バブルソート・選択ソートが正しく昇順に並べ替えること(正常系)
  • 空配列を渡したとき空配列が返ること(境界値)
  • 要素が1つだけの配列を渡したとき同じ配列が返ること(境界値)
  • 既にソート済みの配列を渡したとき正しく同じ順序を維持すること(境界値)
  • 全要素が同じ値の配列でも正常に動作すること(境界値)
  • setStrategy() で戦略を切り替えた後、新しい戦略が適用されること
  • 元の配列が変更されていないこと(コピーして処理していること)

GitHub でソースコードを見る →