概要

オブジェクトの振る舞いを、内部状態に応じて動的に切り替える。State パターンは、状態遷移を if-else や switch の巨大な分岐で管理する代わりに、各状態を独立したクラスとして切り出し、操作を状態オブジェクトに委譲する設計です。自動販売機、注文ステータス管理、ワークフローの承認フローなど、状態遷移が明確に定義される場面で有効です。状態の追加や変更が既存コードに影響しにくくなる一方、状態数が多いとクラスも増えます。この記事では、自動販売機(待機 → コイン投入済み → 払い出し中)を題材に State パターンを実装し、record による遷移ログの記録、sealed interface による状態の型安全な表現まで確認します。

使いどころ

注文ステータス(受付 → 承認 → 出荷 → 完了)の遷移ごとに許可される操作を状態クラスで制御する

自動販売機のコイン投入・商品選択・払い出しの状態遷移を安全に管理する

ドキュメントの承認ワークフロー(下書き → レビュー中 → 承認済み → 公開)で状態ごとに編集可否を切り替える

コード例

StatePatternDemo.java
public class StatePatternDemo {

    interface VendingMachineState {
        void insertCoin(VendingMachine machine);
        void selectProduct(VendingMachine machine, String product);
        void dispense(VendingMachine machine);
    }

    record StateTransition(String from, String to,
                           String action) {}

    static class IdleState implements VendingMachineState {
        @Override
        public void insertCoin(VendingMachine machine) {
            System.out.println("[IDLE] コインを受け取りました");
            machine.setState(new CoinInsertedState());
        }
        @Override
        public void selectProduct(VendingMachine m, String p) {
            System.out.println("[IDLE] コインを投入してください");
        }
        @Override
        public void dispense(VendingMachine m) {
            System.out.println("[IDLE] コインを投入してください");
        }
    }

    static class CoinInsertedState implements VendingMachineState {
        @Override
        public void insertCoin(VendingMachine m) {
            System.out.println("[COIN] 既にコイン投入済みです");
        }
        @Override
        public void selectProduct(VendingMachine m, String p) {
            System.out.println("[COIN] " + p + " を選択");
            m.setProduct(p);
            m.setState(new DispensingState());
        }
        @Override
        public void dispense(VendingMachine m) {
            System.out.println("[COIN] 商品を選択してください");
        }
    }

    static class DispensingState implements VendingMachineState {
        @Override
        public void insertCoin(VendingMachine m) {
            System.out.println("[DISP] 払い出し中です");
        }
        @Override
        public void selectProduct(VendingMachine m, String p) {
            System.out.println("[DISP] 払い出し中です");
        }
        @Override
        public void dispense(VendingMachine m) {
            System.out.println("[DISP] " + m.getProduct()
                             + " を払い出しました");
            m.setProduct(null);
            m.setState(new IdleState());
        }
    }

    static class VendingMachine {
        private VendingMachineState state = new IdleState();
        private String product;
        void setState(VendingMachineState s) { state = s; }
        void setProduct(String p) { product = p; }
        String getProduct() { return product; }
        void insertCoin() { state.insertCoin(this); }
        void selectProduct(String p) {
            state.selectProduct(this, p);
        }
        void dispense() { state.dispense(this); }
    }

    public static void main(String[] args) {
        var machine = new VendingMachine();
        machine.insertCoin();
        machine.selectProduct("コーヒー");
        machine.dispense();

        // 異常操作
        machine.selectProduct("お茶"); // コイン未投入
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record で StateTransition(from / to / action)を定義し、遷移ログを構造化データとして扱える。var も利用可。

Java 17
// Java 17: record で遷移ログを構造化
record StateTransition(String from, String to,
                       String action) {}

public void insertCoin(VendingMachine machine) {
    System.out.println("[IDLE] コインを受け取りました");
    var transition = new StateTransition(
        "IDLE", "COIN_INSERTED", "insertCoin");
    System.out.println(transition);
    machine.setState(new CoinInsertedState());
}

Library Comparison

標準 API(interface + クラス)状態数が少なく遷移ルールが明確な場面。外部依存なしで完結する。状態数が増えるとクラス数も比例して増える。
Spring StateMachine状態遷移の定義・イベント駆動・永続化を統一的に扱いたい大規模なワークフロー。Spring 依存で学習コストが高い。小規模な状態遷移には過剰。
enum + switch による簡易実装状態数が3〜5程度で遷移ロジックが単純な場合。enum のメソッドに遷移ロジックを書ける。状態ごとのデータ保持が難しく、ロジックが増えると enum メソッドが肥大化する。

注意点

State パターンと Strategy パターンは構造が似ているが、State は状態遷移を内部で管理し、Strategy は外部から差し替える点が異なる

状態遷移のルール(どの状態からどの状態に遷移できるか)を図示しておくこと。コードだけでは遷移の全体像が把握しにくい

状態オブジェクトを毎回 new するとオブジェクト生成コストがかかる。状態がステートレスなら Singleton で共有してもよい

不正な操作(例: コイン未投入で商品選択)の処理を各状態クラスで明示的に定義すること。暗黙に無視すると不具合の原因になる

FAQ

State パターンと Strategy パターンの違いは何ですか。

構造は似ていますが、State は状態遷移を内部で自動的に行います。Strategy は利用者が外部からアルゴリズムを差し替えます。状態が自律的に切り替わるかどうかが違いです。

状態オブジェクトは毎回 new すべきですか。

状態がステートレス(フィールドを持たない)なら Singleton で共有できます。状態がデータを保持する場合は毎回生成する必要があります。

enum で State パターンを実装できますか。

抽象メソッドを持つ enum で簡易的に実装可能です。ただし状態ごとのデータ保持が難しく、遷移ロジックが複雑になると管理しにくくなります。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます