Strategy(ストラテジー)パターンは、同じ目的を果たす複数のやり方(アルゴリズム)を1つずつ部品に切り出し、使う側を書き換えずに差し替えられるようにするデザインパターンです。GoFの23パターンのうち「振る舞いに関するパターン」に分類されます。本記事では、if文で配送料の計算方法を切り替えるコードをStrategyに書き換える例から入り、PHP・Java(Spring)・TypeScript・Python・Goの実装、標準ライブラリに組み込まれたStrategy、Stateパターンとの違い、使わない方がよい場面まで、実行して確かめたコードで説明します。
まとめ:Strategyパターンの要点
Strategyパターンは「何をするか」は同じで「どうやるか」だけが複数あるとき、そのやり方を共通のインタフェースの裏に隠し、呼び出す側(Context)からは1つのメソッドを呼ぶだけにする設計です。1994年のGoF本『Design Patterns』で紹介された23パターンの1つで、英語圏ではPolicyパターンとも呼ばれます。使うアルゴリズムの決定を実行時まで遅らせられるのが本質です。
役割はStrategy(共通インタフェース)・ConcreteStrategy(個々のやり方)・Context(使う側)の3つです。やり方が増えたら実装を1つ足すだけで、Contextのif文は増えません。関数を値として渡せる言語では、短い処理ならインタフェースとクラスを作らずに関数1つを戦略にすることもでき、JavaのComparatorやPythonのsorted()のkey引数はその代表です。
ただし、分岐が2〜3個で今後も増えないなら導入しない方が読みやすくなります。判断の目安は「やり方が増え続けるか」「やり方ごとに単体テストしたいか」「実行時に切り替えるか」の3点で、どれも当てはまらなければmatchやswitchで十分です。
Strategyパターンが解決する問題:手段の追加で膨らむif文
ECサイトの配送料を例にします。通常便・速達・店舗受け取りの3種類を、1つの関数の中でif文で切り替えた書き方です。
<?php
declare(strict_types=1);
// 分岐で手段を切り替える(Strategy導入前)
function shippingFee(string $method, int $weightGram): int {
if ($method === 'standard') {
return $weightGram <= 2000 ? 800 : 1200;
} elseif ($method === 'express') {
return 1500 + intdiv($weightGram, 1000) * 200;
} elseif ($method === 'pickup') {
return 0;
}
throw new InvalidArgumentException("未知の配送方法: {$method}");
}
echo shippingFee('express', 2500), PHP_EOL; // 1900
この形は、配送方法が増えるたびに同じ関数へelseifを足すことになります。速達の料金表を直すだけでも、通常便や店舗受け取りと同じ関数を開いて触るため、変更が他の分岐に影響しないか、回帰テストで確認する範囲を検討する必要があります。追加のたびに既存コードへ手を入れる状態は、オープンクローズドの原則(OCP)が避けたい典型です。
Strategyパターンでは、配送方法ごとの計算を別のクラスに分け、注文(Context)は「送料を計算するもの」というインタフェースだけを知る形にします。
<?php
declare(strict_types=1);
// Strategy:配送料の計算方法
interface ShippingStrategy {
public function fee(int $weightGram): int;
}
final class StandardShipping implements ShippingStrategy {
public function fee(int $weightGram): int {
return $weightGram <= 2000 ? 800 : 1200;
}
}
final class ExpressShipping implements ShippingStrategy {
public function fee(int $weightGram): int {
return 1500 + intdiv($weightGram, 1000) * 200;
}
}
final class StorePickup implements ShippingStrategy {
public function fee(int $weightGram): int {
return 0;
}
}
// Context:どの戦略が入っているかを知らない
final class Order {
public function __construct(
private int $weightGram,
private ShippingStrategy $shipping,
) {}
public function changeShipping(ShippingStrategy $shipping): void {
$this->shipping = $shipping;
}
public function shippingFee(): int {
return $this->shipping->fee($this->weightGram);
}
}
$order = new Order(2500, new ExpressShipping());
echo $order->shippingFee(), PHP_EOL; // 1900
$order->changeShipping(new StorePickup());
echo $order->shippingFee(), PHP_EOL; // 0
出力は書き換え前と同じ1900円です。違いは、新しい配送方法(たとえば置き配の割引)を足すときにShippingStrategyを実装したクラスを1つ追加するだけで済み、Orderにも既存の3クラスにも手を入れないことです。changeShipping()で配送方法を差し替えられます。分岐版でも引数を変えれば配送方法を切り替えられますが、Strategy版では注文側から各配送方法の計算処理を分離できます。
Strategyパターンの構造:3つの役割と「誰が戦略を選ぶか」
登場するのは次の3つの役割です。上のPHPコードの名前と対応させています。
| 役割 | 責務 | 上の例 |
|---|---|---|
| Strategy | やり方の共通インタフェース | ShippingStrategy |
| ConcreteStrategy | 個々のやり方の実装 | ExpressShippingなど |
| Context | 戦略を保持して呼び出す | Order |
Contextは戦略をインタフェース型で受け取るので、具体的なクラス名を知りません。このように「使う側が抽象に依存し、具体は外から渡す」形は依存性逆転の原則(DIP)とDI(依存性の注入)そのもので、Strategyパターンが継承ではなく委譲で振る舞いを変える設計だといわれる理由です(継承と委譲の比較は継承とポリモーフィズム・委譲の使い分けで扱っています)。
構造図に出てこないのが「どの戦略を使うかを誰が決めるか」です。Contextの中でifを書いて選ぶと、分岐がContextに戻ってきてしまいます。実務では次のいずれかに寄せます。
- 呼び出し側が選んで渡す:画面で選ばれた配送方法をコントローラが受け取り、対応する戦略を
newして渡す - 登録表(Map)から引く:キーと戦略の対応表を1か所に置き、キーで取り出す。後述のJava・TypeScriptの例がこの形
- DIコンテナに集めさせる:Springなら同じインタフェースの実装を
Mapでまとめて注入できる(後述) - Factoryに任せる:生成手順が重い戦略はファクトリパターンで組み立てる
PHPでのStrategyパターン実装:interface・enum・クロージャ
上のinterfaceと具体クラスによる書き方が基本形です。PHPでは選択肢の性質によって、ほかに2つの書き方を選べます。
PHP 8.1のenumによる固定選択肢の戦略化
会員ランクのように選択肢が決まっていて増減がまれなら、PHP 8.1で入ったenumはinterfaceを実装できるので、enumそのものを戦略にできます。DBに保存した文字列からfrom()でケースを復元すれば、そのまま割引計算に使えます。
<?php
declare(strict_types=1);
interface DiscountStrategy {
public function apply(int $price): int;
}
// 選択肢が固定なら enum に戦略を持たせる(PHP 8.1以降)
enum MemberRank: string implements DiscountStrategy {
case Regular = 'regular';
case Gold = 'gold';
case Platinum = 'platinum';
public function apply(int $price): int {
return match ($this) {
self::Regular => $price,
self::Gold => intdiv($price * 90, 100),
self::Platinum => intdiv($price * 80, 100),
};
}
}
$rank = MemberRank::from('gold'); // DBの値からケースを復元
echo $rank->apply(10000), PHP_EOL; // 9000
echo MemberRank::tryFrom('silver') === null ? 'unknown' : 'ok', PHP_EOL; // unknown
ランクを1つ足すときはcaseとmatchの行を足します。matchはどの行にも当たらない値でUnhandledMatchErrorを投げるので、足し忘れは実行時に止まります。外部APIを呼ぶ・設定値を注入するといった依存を戦略ごとに持たせたい場合は、enumでは扱いにくいのでクラス版にします。
クロージャとcallableによる短い計算戦略の受け渡し
戦略が1〜数行の計算なら、クラスを作らずにクロージャを渡すのが手軽です。PHP標準のusort()は比較関数という戦略を引数で受け取る関数で、標準ライブラリにStrategyが組み込まれている例です。
<?php
declare(strict_types=1);
$products = [
['name' => 'B', 'price' => 300],
['name' => 'A', 'price' => 100],
['name' => 'C', 'price' => 200],
];
// 比較のしかた=戦略。usort() は戦略を引数で受け取る
$byPrice = fn(array $a, array $b): int => $a['price'] <=> $b['price'];
$byName = fn(array $a, array $b): int => strcmp($a['name'], $b['name']);
function sortBy(array $items, callable $compare): array {
usort($items, $compare);
return $items;
}
echo implode(',', array_column(sortBy($products, $byPrice), 'name')), PHP_EOL; // A,C,B
echo implode(',', array_column(sortBy($products, $byName), 'name')), PHP_EOL; // A,B,C
// PHP 8.1 の first-class callable 構文で既存関数をそのまま戦略にする
$names = ['banana', 'Apple', 'cherry'];
usort($names, strcasecmp(...));
echo implode(',', $names), PHP_EOL; // Apple,banana,cherry
最後のstrcasecmp(...)はPHP 8.1のfirst-class callable構文で、既存の関数をそのままClosureにして戦略として渡しています。戦略が状態を持つ、メソッドが2つ以上になる、という時点でクロージャは読みにくくなるので、そこがクラス版へ移す目安です。
JavaとSpring BootでのStrategyパターン実装
Java:関数型インタフェースとラムダによる戦略の実装
Javaでは戦略インタフェースにメソッドが1つだけなら@FunctionalInterfaceを付け、ConcreteStrategyをクラスではなくラムダ式で書けます。戦略の選択はMapの登録表に集めました。以下のコード全体はStream.toList()を使うためJava 16以降が必要です。
import java.util.Comparator;
import java.util.List;
import java.util.Map;
// Strategy:関数型インタフェースにするとラムダで渡せる
@FunctionalInterface
interface ShippingStrategy {
int fee(int weightGram);
}
// Context
class Order {
private final int weightGram;
private ShippingStrategy shipping;
Order(int weightGram, ShippingStrategy shipping) {
this.weightGram = weightGram;
this.shipping = shipping;
}
void changeShipping(ShippingStrategy shipping) { this.shipping = shipping; }
int shippingFee() { return shipping.fee(weightGram); }
}
public class Main {
// 戦略の登録表:キーから戦略を選ぶ責務をここに集める
static final Map<String, ShippingStrategy> STRATEGIES = Map.of(
"standard", w -> w <= 2000 ? 800 : 1200,
"express", w -> 1500 + (w / 1000) * 200,
"pickup", w -> 0
);
public static void main(String[] args) {
Order order = new Order(2500, STRATEGIES.get("express"));
System.out.println(order.shippingFee()); // 1900
order.changeShipping(STRATEGIES.get("pickup"));
System.out.println(order.shippingFee()); // 0
// 標準ライブラリのStrategy:Comparatorを差し替えて並び順を変える
List<String> names = List.of("banana", "apple", "Cherry", "fig");
System.out.println(names.stream().sorted().toList());
System.out.println(names.stream().sorted(String.CASE_INSENSITIVE_ORDER).toList());
System.out.println(names.stream().sorted(Comparator.comparing(String::length)).toList());
}
}
後半の3行は、JDKのComparatorを差し替えるだけで同じsorted()が自然順・大文字小文字を無視した順・長さ順に変わる様子です。sorted()から見ればComparatorは比較の戦略で、Java 8以降はComparator.comparing()のようにメソッド参照から戦略を組み立てられます。短い処理はラムダ式で表し、状態や複数のメソッドを持つ場合、依存関係の注入や実装の再利用が必要な場合はクラスを検討します。
Spring Boot:同じインタフェースのBeanのMap注入
Spring(Spring Bootも同じ)では、同じインタフェースを実装したBeanをMap<String, インタフェース>型でコンストラクタに注入すると、Bean名をキーにした登録表をコンテナが作ってくれます。戦略の選択にifも手書きのMapも要りません。以下はSpring Bootでも利用できるSpring Framework単体の例で、依存関係にorg.springframework:spring-context:7.0.10を追加し、コードをdemo/App.javaとして配置します。
package demo;
import java.util.Map;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
interface PaymentStrategy {
String pay(int amount);
}
@Component("card")
class CardPayment implements PaymentStrategy {
public String pay(int amount) { return amount + "円をカードで決済"; }
}
@Component("bank")
class BankTransfer implements PaymentStrategy {
public String pay(int amount) { return amount + "円を銀行振込で受付"; }
}
@Service
class PaymentService {
// PaymentStrategy を実装した全Beanが「Bean名 → インスタンス」で入る
private final Map<String, PaymentStrategy> strategies;
PaymentService(Map<String, PaymentStrategy> strategies) {
this.strategies = strategies;
}
String pay(String method, int amount) {
PaymentStrategy s = strategies.get(method);
if (s == null) {
throw new IllegalArgumentException("未対応の決済方法: " + method);
}
return s.pay(amount);
}
}
@Configuration
@ComponentScan
public class App {
public static void main(String[] args) {
try (var ctx = new AnnotationConfigApplicationContext(App.class)) {
PaymentService service = ctx.getBean(PaymentService.class);
System.out.println(service.pay("card", 3000));
System.out.println(service.pay("bank", 5000));
}
}
}
Spring Framework 7.0.10で実行すると「3000円をカードで決済」「5000円を銀行振込で受付」と出力されます。新しい決済方法は@Component("paypay")のようにBean名を付けたクラスを1つ足すだけで、PaymentServiceは変わりません。キーは画面やAPIから来る文字列なので、存在しないキーでnullが返る場合の例外処理は必ず書きます。コンテナがBeanを集める仕組みはIoCコンテナとSpring IoCコンテナ・Beanの仕組みで解説しています。
TypeScript・Python・GoでのStrategyパターン実装
TypeScript:関数型とsatisfiesによる登録表の型検査
TypeScriptでは戦略を関数の型として定義し、satisfiesで登録表を検査すると、キーの一覧が型として残ります。存在しない配送方法を渡すとコンパイル時に止まります。
// 戦略は「重さを受け取り送料を返す関数」という型だけで表せる
type ShippingStrategy = (weightGram: number) => number;
const strategies = {
standard: (w) => (w <= 2000 ? 800 : 1200),
express: (w) => 1500 + Math.floor(w / 1000) * 200,
pickup: () => 0,
} satisfies Record<string, ShippingStrategy>;
type ShippingMethod = keyof typeof strategies; // "standard" | "express" | "pickup"
function shippingFee(method: ShippingMethod, weightGram: number): number {
return strategies[method](weightGram);
}
console.log(shippingFee('express', 2500)); // 1900
console.log(shippingFee('pickup', 2500)); // 0
// shippingFee('drone', 100); // コンパイルエラー:存在しないキー
コメントにした最後の行を有効にすると、TypeScript 7.0.2のtsc --strictは「Argument of type ‘”drone”‘ is not assignable」(TS2345)で止まりました。Record<string, ShippingStrategy>と型注釈で書くとキーがstringに広がってこの検査が効かなくなるので、注釈ではなくsatisfiesを使うのが要点です。クラスで書きたい場合は、Javaと同じくinterfaceとimplementsで書けます。
Python:Protocolによる継承不要の戦略インタフェース
Pythonではtyping.Protocol(Python 3.8以降)で戦略の形だけを宣言できます。具体クラスはProtocolを継承しなくても、fee()の引数と戻り値の型がProtocolの宣言に適合していれば、mypyなどの型チェッカーが戦略として受け入れます。
from dataclasses import dataclass
from typing import Protocol
class ShippingStrategy(Protocol):
def fee(self, weight_gram: int) -> int: ...
class Standard:
def fee(self, weight_gram: int) -> int:
return 800 if weight_gram <= 2000 else 1200
class Express:
def fee(self, weight_gram: int) -> int:
return 1500 + (weight_gram // 1000) * 200
@dataclass
class Order:
weight_gram: int
shipping: ShippingStrategy # 継承なしでも fee() の型がProtocolに適合すれば渡せる
def shipping_fee(self) -> int:
return self.shipping.fee(self.weight_gram)
order = Order(2500, Express())
print(order.shipping_fee()) # 1900
order.shipping = Standard()
print(order.shipping_fee()) # 1200
# 標準ライブラリのStrategy:key関数を差し替えて並び順を変える
names = ["banana", "apple", "Cherry", "fig"]
print(sorted(names)) # ['Cherry', 'apple', 'banana', 'fig']
print(sorted(names, key=str.lower)) # ['apple', 'banana', 'Cherry', 'fig']
print(sorted(names, key=len)) # ['fig', 'apple', 'banana', 'Cherry']
Pythonでは関数がそのまま値なので、戦略が1つのメソッドで済むならCallable[[int], int]型の関数を渡す方が短く書けます。組み込みのsorted()のkey引数は、並べ替えのアルゴリズムはそのままに比較の基準だけを差し替える戦略で、末尾の3行がその例です。
Go:関数型の値による戦略の差し替え
Goはインタフェースでも書けますが、メソッドが1つの戦略なら関数型を定義して関数値を渡す方が簡潔です。
package main
import (
"cmp"
"fmt"
"slices"
"strings"
)
// 戦略を関数型で表す
type ShippingStrategy func(weightGram int) int
type Order struct {
WeightGram int
Shipping ShippingStrategy
}
func (o *Order) ShippingFee() int { return o.Shipping(o.WeightGram) }
func main() {
express := func(w int) int { return 1500 + (w/1000)*200 }
pickup := func(int) int { return 0 }
o := &Order{WeightGram: 2500, Shipping: express}
fmt.Println(o.ShippingFee()) // 1900
o.Shipping = pickup
fmt.Println(o.ShippingFee()) // 0
// 標準ライブラリのStrategy:比較関数を差し替える(Go 1.21以降)
names := []string{"banana", "apple", "Cherry", "fig"}
slices.SortFunc(names, func(a, b string) int {
return cmp.Compare(strings.ToLower(a), strings.ToLower(b))
})
fmt.Println(names) // [apple banana Cherry fig]
}
この例では呼び出し側で戦略フィールドを直接差し替えています。戦略を変更するメソッドを追加する場合は、受け手をポインタ(*Order)にします。値で受けるとコピーに代入するだけで、呼び出し元の戦略は変わりません。後半のslices.SortFunc(Go 1.21で追加)も比較関数を受け取るStrategyです。
標準ライブラリとフレームワークに組み込まれたStrategy
Strategyパターンに対応付けて理解できる仕組みは、標準ライブラリや認証フレームワークにもあります。共通の契約を通して同じ目的の処理方法を交換できる箇所は、Strategyの観点から構造を読み解けます。
| 言語・ライブラリ | 戦略 | 差し替わるもの |
|---|---|---|
| Java | Comparator |
並べ替えの比較基準 |
| PHP | usort()の比較関数 |
並べ替えの比較基準 |
| Python | sorted()のkey |
並べ替えの比較キー |
| Go | slices.SortFuncの比較関数 |
並べ替えの比較基準 |
| Passport.js | Strategy(認証方式) | ログインの方法 |
| Spring Security | PasswordEncoder |
パスワードのハッシュ方式 |
認証ライブラリの例も同じ構造です。Node.jsのPassport.jsは、登録した戦略の名前をpassport.authenticate()の第1引数に渡して認証方式を選びます。ID・パスワード、Google、SAMLといった方式はそれぞれ独立したパッケージの戦略で、公式サイトは「500+ Strategies」と案内しています。Spring SecurityのDelegatingPasswordEncoderは、保存済みパスワードの先頭にある{bcrypt}のようなidを見て照合に使うPasswordEncoderを選びます。PasswordEncoderFactories.createDelegatingPasswordEncoder()で作ると新しいパスワードはbcryptでハッシュ化され、登録済みのidに対応する旧方式のハッシュも照合できます。ただし、既存ハッシュの移行には新方式の指定に加え、認証時などに再ハッシュして保存する処理が必要です。
これらの例では、利用側の呼び出し方を共通に保ちながら、比較・認証・ハッシュ化などの処理方法を差し替えています。自作の設計で迷ったら、この表のように「差し替わるもの」を1語で言えるかを確かめてください。言えないなら、戦略の境界の切り方がまだ曖昧です。
他パターンとの違い:State・Template Method・Command・Factory
Strategyパターンはクラス図だけ見ると他のパターンとほとんど同じに見えるため、違いは目的と「誰が切り替えるか」で見分けます。
| パターン | 差し替える単位 | 切り替える主体 | 手段 |
|---|---|---|---|
| Strategy | アルゴリズム全体 | 利用側・DI | 委譲 |
| State | 状態ごとの振る舞い | Contextまたは状態オブジェクト | 委譲 |
| Template Method | 手順の一部 | サブクラス(固定) | 継承 |
| Command | 要求そのもの | 呼び出し側 | 委譲 |
| Factory Method | 生成するオブジェクト | サブクラス | 継承 |
Stateパターンとの違い:処理方法の交換と状態別の振る舞い
StateパターンとStrategyパターンは、どちらもContextが仕事の一部を別のオブジェクトに任せる構造で、クラス図はほぼ同じです。主な違いは目的で、Strategyは処理方法の交換、Stateは内部状態に応じた振る舞いの変更を扱います。Strategyでは戦略を選ぶのは外側(利用者やDIコンテナ)で、個々の戦略は完全に独立していて互いを知りません。Stateでは状態オブジェクトが互いを知っていてよく、「支払い済み」の状態が自分で「発送待ち」へContextの状態を切り替えるように、状態オブジェクト側で遷移を扱う実装があります。ただし、遷移条件をContext側に置くことや、利用側が状態を設定することも可能です。
見分け方は「同じ目的の処理方法を交換したいのか、内部状態ごとの振る舞いを表したいのか」です。注文の状態や接続の状態のように遷移の規則そのものを設計したい場合は、ステートマシンの設計と実装パターンを参照してください。
Template Method・Command・Factoryとの違い
Template Methodは処理の骨組みを親クラスに固定し、一部の手順だけをサブクラスで上書きします。どの手順を使うかはインスタンスを作るクラスで決まり、Strategyのように生成済みのオブジェクトの参照を差し替えて途中で交換することはできません。手順の順番そのものを共通化したいならTemplate Method、処理全体を丸ごと交換したいならStrategyです(詳しい使い分けはテンプレートメソッドパターンとStrategyとの使い分け)。Commandは「何をするか」という要求をオブジェクトにして、キューに積む・取り消すといった扱いを可能にするパターンで、「同じことをどうやるか」を選ぶStrategyとは目的が違います。Factoryはオブジェクトを作る側のパターンなので競合せず、前述のとおり戦略を組み立てる役で一緒に使います。23パターン全体の分類はGoFデザインパターン23種類の一覧にまとめています。
Strategyパターンを使うべき場面・使わない方がよい場面
本記事では、次の条件が複数そろう場合を導入検討の目安とします。ただし、1つだけでも変更の影響を切り離す効果が大きければ候補になります。
- 同じ目的のやり方が3つ以上あり、今後も増える(決済手段・配送方法・料金プラン・通知チャネル・ファイル出力形式など)
- やり方ごとに単体テストを分けたい、または別のチームが別のやり方を担当する
- 設定値や利用者の選択によって実行時に切り替える
逆に、分岐が2〜3個で増える見込みがないなら導入しません。match式ひとつで読めるロジックをインタフェースと3つのクラスに分けると、処理を追うために4ファイルを開くことになります。「将来増えるかもしれない」という推測だけで抽象化すると、実際に増えたときの形が想定と違って作り直しになることも多く、増えた時点でif文からStrategyへ書き換える方が安く済みます。書き換え自体は、上の配送料の例のように機械的に進められます。
もう1つの失敗は、戦略ごとに必要な引数が違うのに無理に1つのインタフェースにまとめることです。カード決済にはカード番号、銀行振込には口座が要るといった違いをpay(array $options)のような何でも入る引数で吸収すると、型による保護が消えて呼び出し側が戦略の中身を知る必要が出てきます。そうなったら、引数の違いは戦略のコンストラクタに持たせ、インタフェースのメソッドは全戦略で共通のものだけにします。業務システムで決済や料金計算のように拡張が続く部分の設計から相談したい場合は、フルスクラッチ開発の支援もご覧ください。
よくある質問
Strategyパターンとは簡単に言うと何ですか?
同じ目的を果たす複数のやり方を部品に分け、使う側を変えずに差し替えられるようにする設計です。if文で「カードならこう、振込ならこう」と書いていた分岐を、共通インタフェースを実装したクラスや関数に置き換えます。
PHPでStrategyパターンはどう実装しますか?
基本は戦略のinterfaceを定義し、やり方ごとに実装クラスを作って、利用側がinterface型で受け取って呼び出します。選択肢が固定ならPHP 8.1以降のenumにinterfaceを実装させる形、1行程度の計算ならクロージャをcallableとして渡す形も使えます。
StrategyパターンとStateパターンの違いは何ですか?
Strategyはどのやり方を使うかを外側(利用側やDIコンテナ)が決め、戦略同士は互いを知りません。Stateは内部状態に応じた振る舞いを状態オブジェクトに分け、次の状態はContextまたは状態オブジェクトが決めます。処理方法の交換が目的ならStrategy、内部状態ごとの振る舞いの整理が目的ならStateです。
Spring BootでStrategyパターンを使うには?
戦略インタフェースの実装クラスに@Component("名前")を付け、利用側のコンストラクタでMap<String, 戦略インタフェース>を受け取ります。Bean名がキーになるので、map.get(キー)で戦略を選べます。Listで受け取ればすべての戦略を順に試す形にもできます。
Strategyパターンのデメリットは?
クラスやファイルが増えて処理の流れを追いにくくなることと、どの戦略を選ぶかという責務を別の場所に置く必要があることです。分岐が少なく増えない箇所に入れると、得られるものより読みにくさの方が大きくなります。