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 での利用シーン
- ゲームのキャラクター状態管理(通常・ダメージ・無敵・死亡)
- ワークフローの承認フロー(申請中・承認待ち・承認済み・却下)
- 注文ステータス管理(注文受付・支払い待ち・発送準備中・発送済み・完了)
- ネットワーク接続状態(未接続・接続中・接続済み・切断中)
サンプルコード
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 に戻り、再度コインを投入できること(状態のリセット確認)
- 複数回フルフローを繰り返しても正しく動作すること(状態のリーク(残り)がないこと)