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行で戦略を定義できます。
サンプルコード
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()で戦略を切り替えた後、新しい戦略が適用されること- 元の配列が変更されていないこと(コピーして処理していること)