java-recipes

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

DP-20: State パターン

オブジェクトの「状態」に応じて振る舞いを切り替えるパターンです。 状態ごとにクラスを分けることで、状態数が増えても if-else が膨らまず、各状態の責務が明確になります。

State パターンとは

State パターンは、オブジェクトが取り得る「状態」をそれぞれクラスとして表現し、状態に応じた振る舞いを各クラスに持たせるデザインパターンです。 オブジェクトは現在の状態オブジェクトに処理を委譲(いじょう)するだけでよいため、状態が増えても既存のコードを変更せず新しい状態クラスを追加するだけで対応できます。

状態遷移図(自動販売機の例)

IDLE
  ↓ insertCoin(コイン投入)
COIN_INSERTED
  ↓ selectProduct(商品選択)
DISPENSING
  ↓ dispense(払い出し)
IDLE(元の状態に戻る)

各状態で無効な操作はエラーメッセージを返し、状態を変えない。
例: IDLE 状態で selectProduct → 「コインを投入してください」

if-else による状態管理との比較

State パターンを使わずに if-else で状態管理をすると、状態が増えるにつれてコードが複雑になっていきます。

// if-else による状態管理(アンチパターン)
// 状態が増えるたびに全メソッドの if-else が膨らむ
public void insertCoin() {
    if (state.equals("IDLE")) {
        state = "COIN_INSERTED";
    } else if (state.equals("COIN_INSERTED")) {
        System.out.println("既にコインが投入されています。");
    } else if (state.equals("DISPENSING")) {
        System.out.println("払い出し中です。");
    }
    // 新しい状態が増えるたびにここに追記が必要…
}

// State パターン(推奨)
// 各状態クラスが自分の振る舞いを持つ
// → 新しい状態は新しいクラスを追加するだけ
public void insertCoin() {
    state.insertCoin(this); // 現在の状態に処理を委譲
}

Java での利用シーン

  • ゲームのキャラクター状態管理(通常・ダメージ・無敵・死亡)
  • ワークフローの承認フロー(申請中・承認待ち・承認済み・却下)
  • 注文ステータス管理(注文受付・支払い待ち・発送準備中・発送済み・完了)
  • ネットワーク接続状態(未接続・接続中・接続済み・切断中)

サンプルコード

StatePatternSample.java
public class StatePatternSample {

    // 自動販売機の状態を表すインターフェース
    interface VendingMachineState {
        // コインを投入する操作
        void insertCoin(VendingMachine machine);
        // 商品を選択する操作
        void selectProduct(VendingMachine machine, String product);
        // 商品を払い出す操作
        void dispense(VendingMachine machine);
    }

    // 状態1: コイン未投入(待機中)
    static class IdleState implements VendingMachineState {
        @Override
        public void insertCoin(VendingMachine machine) {
            System.out.println("[IDLE] コインを受け取りました。商品を選んでください。");
            // CoinInsertedState へ遷移する
            machine.setState(new CoinInsertedState());
        }

        @Override
        public void selectProduct(VendingMachine machine, String product) {
            // コイン未投入なので商品を選べない
            System.out.println("[IDLE] コインを投入してください。");
        }

        @Override
        public void dispense(VendingMachine machine) {
            // コイン未投入なので払い出せない
            System.out.println("[IDLE] コインを投入してください。");
        }
    }

    // 状態2: コイン投入済み(商品待ち)
    static class CoinInsertedState implements VendingMachineState {
        @Override
        public void insertCoin(VendingMachine machine) {
            // すでにコインが入っている
            System.out.println("[COIN_INSERTED] 既にコインが投入されています。");
        }

        @Override
        public void selectProduct(VendingMachine machine, String product) {
            System.out.println("[COIN_INSERTED] 「" + product + "」を選択しました。");
            // 選択した商品を記録し、DispensingState へ遷移する
            machine.setSelectedProduct(product);
            machine.setState(new DispensingState());
        }

        @Override
        public void dispense(VendingMachine machine) {
            // 商品が未選択なので払い出せない
            System.out.println("[COIN_INSERTED] 商品を選択してください。");
        }
    }

    // 状態3: 商品払い出し中
    static class DispensingState implements VendingMachineState {
        @Override
        public void insertCoin(VendingMachine machine) {
            // 払い出し中はコインを受け付けない
            System.out.println("[DISPENSING] 払い出し中です。しばらくお待ちください。");
        }

        @Override
        public void selectProduct(VendingMachine machine, String product) {
            // 払い出し中は商品を選べない
            System.out.println("[DISPENSING] 払い出し中です。しばらくお待ちください。");
        }

        @Override
        public void dispense(VendingMachine machine) {
            String product = machine.getSelectedProduct();
            System.out.println("[DISPENSING] 「" + product + "」を払い出しました。ありがとうございました。");
            // 払い出しが完了したら IdleState に戻る
            machine.setSelectedProduct(null);
            machine.setState(new IdleState());
        }
    }

    // 自動販売機本体: 現在の状態と選択商品を保持する
    static class VendingMachine {
        // 現在の状態
        private VendingMachineState state;
        // 選択された商品
        private String selectedProduct;

        public VendingMachine() {
            // 初期状態は IdleState(待機中)
            this.state = new IdleState();
            this.selectedProduct = null;
        }

        public void setState(VendingMachineState state) {
            this.state = state;
        }

        public void setSelectedProduct(String product) {
            this.selectedProduct = product;
        }

        public String getSelectedProduct() {
            return selectedProduct;
        }

        // 操作メソッド: 現在の状態に処理を委譲する
        public void insertCoin() {
            state.insertCoin(this);
        }

        public void selectProduct(String product) {
            state.selectProduct(this, product);
        }

        public void dispense() {
            state.dispense(this);
        }
    }

    public static void main(String[] args) {
        System.out.println("=== State パターン: 自動販売機 ===\n");

        VendingMachine machine = new VendingMachine();

        System.out.println("--- 正常フロー: コイン投入 → 商品選択 → 払い出し ---");
        machine.insertCoin();                      // IDLE → COIN_INSERTED
        machine.selectProduct("コーヒー");          // COIN_INSERTED → DISPENSING
        machine.dispense();                        // DISPENSING → IDLE

        System.out.println();
        System.out.println("--- 異常操作: コイン未投入で商品を選ぼうとする ---");
        machine.selectProduct("お茶");             // IDLE: コインを投入してください

        System.out.println();
        System.out.println("--- 異常操作: コイン二重投入 ---");
        machine.insertCoin();                      // IDLE → COIN_INSERTED
        machine.insertCoin();                      // 既にコインが入っている

        System.out.println();
        System.out.println("--- 異常操作: 商品未選択で払い出しを試みる ---");
        machine.dispense();                        // 商品を選択してください
        machine.selectProduct("ジュース");          // COIN_INSERTED → DISPENSING
        machine.dispense();                        // DISPENSING → IDLE
    }
}

Java 8 では VendingMachineState インターフェースと各状態クラス(IdleState / CoinInsertedState / DispensingState)を定義します。各状態クラスが自分の状態に応じた振る舞いを実装し、VendingMachine 本体は現在の状態に処理を委譲するだけです。

よくあるミス・注意点

⚠️ 無効な状態遷移のチェックを忘れる

例えば「コイン未投入状態で商品を選択する」など、本来できないはずの操作を黙って無視してしまうと、 利用者はなぜ動かないのか分からなくなります。 無効な操作には必ずエラーメッセージや例外で「今の状態ではこの操作はできません」と伝えましょう。 Java 21 の sealed interface + switch を使うと、全状態のパターンが網羅されているかコンパイラがチェックしてくれます。

⚠️ 状態クラスに Context(自動販売機本体)のフィールドを直接持たせる

状態クラスが Context(VendingMachine など)の内部データに直接アクセスしすぎると、 状態クラスと Context が密結合(べったりくっついた関係)になってしまいます。 状態クラスから Context を操作するときは、setState()setSelectedProduct() のような 公開メソッドを通じて操作するようにしましょう。

⚠️ Strategy パターンと混同する

State パターンと Strategy パターンは構造が似ていますが、目的が異なります。 State パターンは「状態の遷移」を管理し、状態クラス自身が次の状態へ切り替える責務を持ちます。 一方 Strategy パターンは「アルゴリズムの切り替え」が目的で、外部からどの戦略を使うか選択します。 「自分で状態を変える」かどうかが見分けのポイントです。

テストする観点

  • 各状態で有効な操作が正しく動作すること(例: IDLE 状態で insertCoin() を呼ぶと COIN_INSERTED 状態に遷移する)
  • 各状態で無効な操作がエラーメッセージを出力し、状態が変わらないこと(例: IDLE 状態で selectProduct() を呼んでも状態は IDLE のまま)
  • 正常フロー全体(IDLE → COIN_INSERTED → DISPENSING → IDLE)が期待通りに完了すること
  • コイン二重投入(COIN_INSERTED 状態で insertCoin() を呼ぶ)がエラーになること
  • 払い出し後に状態が IDLE に戻り、再度コインを投入できること(状態のリセット確認)
  • 複数回フルフローを繰り返しても正しく動作すること(状態のリーク(残り)がないこと)

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