ソフトウェア設計

Strategyパターン vs Stateパターン、構造は同じなのに何が違う?

Strategyパターンは外部からアルゴリズムを選択し、Stateパターンは現在の状態が内部の遷移を主導します。同じクラス構造を持つ2つの振る舞いパターンを、意図と変更の主体から見分けます。

読了 5 分
Strategyパターン vs Stateパターン、構造は同じなのに何が違う?のカバー画像

デザインパターンを学んでいると、ほぼ一度は壁にぶつかるポイントがあります。

それがStrategyパターンとStateパターンです。

UML(Unified Modeling Language、統一モデリング言語)の図を並べて見ると、ほとんどコピー&ペーストしたように同じです。インターフェースが1つあり、その実装を複数作り、コンテキストが保持します。

「これ、名前が違うだけでは?」と思うのも無理はありません。

まず結論から言います。2つのパターンを分けるのは構造ではなく意図です。Strategyは外からアルゴリズムを差し替え、Stateは内部で状態自身が次の状態へ遷移します。

この一文を押さえて読み進めれば、もう混同しないはずです。

状態オブジェクトで実際の条件分岐を取り除く実装は、Swift Stateパターンのリファクタリングで段階的に解説しています。

なぜ構造がこれほど同じなのでしょうか?

どちらもGoF(Gang of Four)のデザインパターン本に登場する振る舞いパターンです。

どちらも同じ材料を使います。共通インターフェース、それを実装する複数のクラス、そしてそれらを保持するコンテキストです。

そのため、コードだけを見ると本当に区別できません。

Strategyパターンをコードで表すと、次のようになります。

protocol PaymentStrategy {
    func pay(_ amount: Int)
}

struct CardPayment: PaymentStrategy {
    func pay(_ amount: Int) { print("カードで \(amount)ウォン決済") }
}

struct KakaoPay: PaymentStrategy {
    func pay(_ amount: Int) { print("Kakao Payで \(amount)ウォン決済") }
}

final class Checkout {
    var strategy: PaymentStrategy
    init(strategy: PaymentStrategy) { self.strategy = strategy }
    func pay(_ amount: Int) { strategy.pay(amount) }
}

let checkout = Checkout(strategy: CardPayment())
checkout.pay(10000)
checkout.strategy = KakaoPay()  // 外から差し替え
checkout.pay(5000)
// 出力:
// カードで 10000ウォン決済
// Kakao Payで 5000ウォン決済

ここで注目すべきは、strategyを差し替える主体です。外側のクライアントコードです。

カードからKakao Payへ支払い方法を変えたのは、開発者、つまり外側にいる誰かです。

Strategy自身は次に何が来るかをまったく知りません。関心もありません。


では、Stateパターンは何が違うのでしょうか?

Stateパターンでは、状態自身が状態遷移を決定します。

信号機を思い浮かべてください。赤信号は次が青信号だと知っていて、青信号は次が黄色信号だと知っています。

つまり、次の状態へ移るルールが各状態の中に入っています。

状態が自ら次へ進む流れ、これがStateの正体です
状態が自ら次へ進む流れ、これがStateの正体です
protocol TrafficState {
    func next(_ light: TrafficLight)
}

final class Red: TrafficState {
    func next(_ light: TrafficLight) {
        print("赤 → 青")
        light.state = Green()  // 状態が自ら次を指定
    }
}

final class Green: TrafficState {
    func next(_ light: TrafficLight) {
        print("青 → 黄")
        light.state = Yellow()
    }
}

final class Yellow: TrafficState {
    func next(_ light: TrafficLight) {
        print("黄 → 赤")
        light.state = Red()
    }
}

final class TrafficLight {
    var state: TrafficState = Red()
    func change() { state.next(self) }
}

let light = TrafficLight()
light.change()
light.change()
light.change()
// 出力:
// 赤 → 青
// 青 → 黄
// 黄 → 赤

違いが見えてきましたか?

Strategyパターンでは、checkout.strategy = KakaoPay()のように外から直接差し替えました。

Stateパターンでは、light.state = Green()を状態クラス自身が行います。クライアントはchange()を呼ぶだけです。

まさにここが核心です。

Strategyは外から差し替え、Stateは内側で自ら遷移します。


決定的な違いを3つに整理

言葉だけではまた混乱するので、表にまとめます。

区分 Strategyパターン Stateパターン
意図 アルゴリズムを交換 状態に応じて振る舞いを変える
遷移の主体 外部クライアント 状態オブジェクト自身
オブジェクト間の関係 互いを知らない(独立) 互いを知り、参照する
ライフサイクル 通常は一度決めたら維持 実行中も変化し続ける

特に2行目と3行目を覚えておいてください。

Strategy同士は互いの存在を知りません。カード決済がKakao Payを知る必要はありません。

一方、State同士は互いを知る必要があります。赤信号が青信号オブジェクトを直接生成して渡すからです。

この「互いを知っているかどうか」が、コード上で2つのパターンを見分ける最も確実な手がかりです。

2つのコードを並べて見ると、「誰が変えるのか」が見えてきます
2つのコードを並べて見ると、「誰が変えるのか」が見えてきます

いつ使い、いつ避けるべきでしょうか?

実務で私が判断するときの基準をまとめると、次のとおりです。

状況 判断
同じことを行う方法だけが複数ある場合(ソート、決済、圧縮) Strategyパターン
オブジェクトが状況に応じて異なる振る舞いをし、その状況が変わる場合 Stateパターン
遷移ルールが複雑なif-elseの塊になっている場合 Stateパターンで解決
関数を1つ渡すだけで済むほど単純な場合 どちらも過剰、クロージャを使う

注意点もあります。

状態が2~3個だけで遷移も単純なら、無理にパターンを導入せず、enumとswitchで十分なことが多いです。

パターンは複雑さを制御する道具であって、単純なコードに形式を与える飾りではありません。

面接ではこう聞かれます

Q. StrategyパターンとStateパターンは構造が同じですが、どう区別しますか?

構造はほぼ同じですが、意図が異なります。Strategyは外部からアルゴリズムを交換するためのもので、Stateは内部状態に応じて振る舞いを変えるためのものです。決定的に異なるのは遷移の主体で、Strategyではクライアントが、Stateでは状態オブジェクト自身が次へ進みます。

Q. Stateパターンでは状態同士が互いを参照しますが、問題ありませんか?

状態間に結合が生じるのは確かです。そのため、遷移ロジックを状態内に置く代わりに、コンテキストや別の遷移テーブルへ切り出して結合を下げることもあります。状態が増え、遷移が複雑に絡み合うなら、ステートマシンライブラリを使う方法もあります。


図が同じように見えても怖がらないでください。「誰が遷移するのか」だけを尋ねれば、答えはすぐ出ます。

外から差し替えればStrategy、内側で自ら遷移すればState。この一文だけ覚えて帰れば、今日の記事は成功です。また混同しやすいパターンを取り上げます。

あわせて読みたい