概要
オブジェクトの振る舞いを、内部状態に応じて動的に切り替える。State パターンは、状態遷移を if-else や switch の巨大な分岐で管理する代わりに、各状態を独立したクラスとして切り出し、操作を状態オブジェクトに委譲する設計です。自動販売機、注文ステータス管理、ワークフローの承認フローなど、状態遷移が明確に定義される場面で有効です。状態の追加や変更が既存コードに影響しにくくなる一方、状態数が多いとクラスも増えます。この記事では、自動販売機(待機 → コイン投入済み → 払い出し中)を題材に State パターンを実装し、record による遷移ログの記録、sealed interface による状態の型安全な表現まで確認します。
使いどころ
注文ステータス(受付 → 承認 → 出荷 → 完了)の遷移ごとに許可される操作を状態クラスで制御する
自動販売機のコイン投入・商品選択・払い出しの状態遷移を安全に管理する
ドキュメントの承認ワークフロー(下書き → レビュー中 → 承認済み → 公開)で状態ごとに編集可否を切り替える
コード例
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("お茶"); // コイン未投入
}
}Version Coverage
record で StateTransition(from / to / action)を定義し、遷移ログを構造化データとして扱える。var も利用可。
// 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
注意点
State パターンと Strategy パターンは構造が似ているが、State は状態遷移を内部で管理し、Strategy は外部から差し替える点が異なる
状態遷移のルール(どの状態からどの状態に遷移できるか)を図示しておくこと。コードだけでは遷移の全体像が把握しにくい
状態オブジェクトを毎回 new するとオブジェクト生成コストがかかる。状態がステートレスなら Singleton で共有してもよい
不正な操作(例: コイン未投入で商品選択)の処理を各状態クラスで明示的に定義すること。暗黙に無視すると不具合の原因になる
FAQ
構造は似ていますが、State は状態遷移を内部で自動的に行います。Strategy は利用者が外部からアルゴリズムを差し替えます。状態が自律的に切り替わるかどうかが違いです。
状態がステートレス(フィールドを持たない)なら Singleton で共有できます。状態がデータを保持する場合は毎回生成する必要があります。
抽象メソッドを持つ enum で簡易的に実装可能です。ただし状態ごとのデータ保持が難しく、遷移ロジックが複雑になると管理しにくくなります。