State パターン
オブジェクトの内部状態に応じて振る舞いを切り替え、状態遷移を明示的にモデル化するデザインパターン
設計パターンフロントエンド
State パターンとは
State パターンは、オブジェクトの振る舞いを内部状態に応じて切り替える GoF デザインパターンである。if-else や switch 文で状態を判定する代わりに、各状態をクラスとして表現し、状態遷移を明示的にモデル化する。
Before / After
// ❌ switch 文: 状態が増えるたびに全メソッドに分岐を追加
class Order {
status: 'draft' | 'pending' | 'paid' | 'shipped' | 'cancelled';
confirm() {
switch (this.status) {
case 'draft': this.status = 'pending'; break;
case 'pending': throw new Error('Already confirmed');
case 'paid': throw new Error('Already paid');
// ... 状態が増えるたびに分岐が増える
}
}
pay() {
switch (this.status) {
case 'pending': this.status = 'paid'; break;
// ... 同じパターンの繰り返し
}
}
}
// ✅ State パターン: 各状態がクラス、遷移が明示的
interface OrderState {
confirm(order: Order): void;
pay(order: Order): void;
ship(order: Order): void;
cancel(order: Order): void;
}
class DraftState implements OrderState {
confirm(order: Order) { order.setState(new PendingState()); }
pay() { throw new Error('Cannot pay a draft order'); }
ship() { throw new Error('Cannot ship a draft order'); }
cancel(order: Order) { order.setState(new CancelledState()); }
}
class PendingState implements OrderState {
confirm() { throw new Error('Already confirmed'); }
pay(order: Order) { order.setState(new PaidState()); }
ship() { throw new Error('Cannot ship before payment'); }
cancel(order: Order) { order.setState(new CancelledState()); }
}
状態遷移図
[Draft] ──confirm──→ [Pending] ──pay──→ [Paid] ──ship──→ [Shipped]
│ │ │
└──cancel──→ [Cancelled] ←──cancel──┘ │
└──cancel──→ [Cancelled]
State パターンを使うと、この状態遷移図がコードに直接反映される。各状態クラスが「この状態から何ができるか」を定義する。
Strategy パターンとの違い
| パターン | 目的 | 切り替えのタイミング |
|---|---|---|
| State | 内部状態に応じて振る舞いを変える | 状態遷移時に自動的に切り替わる |
| Strategy | アルゴリズムを外部から差し替える | クライアントが明示的に選択する |
State は「注文が支払い済みになったら、自動的に PaidState の振る舞いに切り替わる」。Strategy は「ソートアルゴリズムをクイックソートからマージソートに差し替える」。
TypeScript の Discriminated Union による代替
TypeScript では、State パターンをクラスではなく Discriminated Union で表現する方が簡潔な場合がある。
type OrderState =
| { status: 'draft' }
| { status: 'pending'; confirmedAt: Date }
| { status: 'paid'; paidAt: Date }
| { status: 'shipped'; shippedAt: Date }
| { status: 'cancelled'; cancelledAt: Date };
function confirm(state: OrderState): OrderState {
if (state.status !== 'draft') throw new Error(`Cannot confirm from ${state.status}`);
return { status: 'pending', confirmedAt: new Date() };
}
Discriminated Union は不変 (Immutable) で、パターンマッチで網羅性チェックが効く。
ステートパターン vs switch 文
| 観点 | switch 文 | ステートパターン |
|---|---|---|
| 拡張性 | 分岐を追加 | 新しいクラスを追加 |
| 開放閉鎖原則 | 違反 | 準拠 |
| テスト | 全分岐をテスト | 状態ごとにテスト |
State パターンを扱う関連書籍も多い。