📢 この記事は gemini-3-flash-preview によって翻訳されました
State Pattern オブジェクト振る舞い型パターン
意図
オブジェクトの内部状態が変化したときに、そのオブジェクトの振る舞いを変えられるようにする。まるでオブジェクトがそのクラスを書き換えたかのように見えるよ。
構成

構成要素はこんな感じ:
Context (コンテキスト): クライアントが利用するインターフェースを定義する。現在の状態を表す ConcreteState サブクラスのインスタンスを保持しているよ。
State (状態): Context の特定の状態に関連する振る舞いをカプセル化するためのインターフェースを定義する。
ConcreteState (具体状態サブクラス): 各サブクラスが、Context の一つの状態に対応する振る舞いを実装する。
どんな時に使う?
オブジェクトの振る舞いがその時の状態によって決まり、実行時にその状態に応じて振る舞いを変える必要があるとき。
ある操作の中に、オブジェクトの状態に依存する巨大な多分岐の条件文(if-else や switch)があるとき。この状態は通常、1つ以上の列挙定数で表される。State パターンは、それぞれの条件分岐を独立したクラスに分けることで、状態を一つのオブジェクトとして扱えるようにするんだ。そうすれば、他のオブジェクトに依存せずに状態を変化させることができるようになるよ。
例 1
あるショッピングモールに、2元で1パックのティッシュを売るシンプルな自販機があるとする。一度に1パックだけ売れる仕様だ。自販機の状態遷移図はこんな感じ。

このティッシュ自販機を State パターンで実装すると、次のようなクラス図になる。State クラスは抽象クラスで、コイン投入、コイン返却、ボタン押し、ティッシュ出す、といったメソッドを定義している。SoldState(販売中)、SoldOutState(売り切れ)、NoQuarterState(コインなし)、HasQuarterState(コインあり)の4つのクラスが、それぞれの状態に対応しているよ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| import java.util.*;
interface State {
public void insertQuarter(); // コイン投入
public void ejectQuarter(); // コイン返却
public void turnCrank(); // 「ティッシュを出す」ボタンを押す
public void dispense(); // ティッシュを出す
}
class TissueMachine {
State soldOutState, noQuarterState, hasQuarterState, soldState, state;
state = soldOutState;
int count = 0; // ティッシュの数
public TissueMachine(int numbers) { /* 実装は省略 */ }
public State getHasQuarterState() { return hasQuarterState; }
public State getNoQuarterState() { return noQuarterState; }
public State getSoldState() { return soldState; }
public State getSoldOutState() { return soldOutState; }
public int getCount() { return count; }
// 残りのコードは省略
}
class NoQuarterState implements State {
TissueMachine tissueMachine;
public void insertQuarter() {
tissueMachine.setState(tissueMachine.getHasQuarterState());
}
// コンストラクタなどは省略
}
class HasQuarterState implements State {
TissueMachine tissueMachine;
public void ejectQuarter() {
tissueMachine.setState(tissueMachine.getNoQuarterState());
}
// コンストラクタなどは省略
}
class SoldState implements State {
TissueMachine tissueMachine;
public void dispense() {
if (tissueMachine.getCount() > 0) {
tissueMachine.setState(tissueMachine.getNoQuarterState());
} else {
tissueMachine.setState(tissueMachine.getSoldOutState()); }
}
}
|
例 2
ある航空会社のマイレージシステムでは、会員を「一般 (Basic)」「シルバー (Silver)」「ゴールド (Gold)」の3つのランクに分けている。非会員 (NonMember) は一般会員の申請ができる。会員ランクは、1年間の累積マイル数によって調整される。ランク変動の状態遷移図はこんな感じ。

これを State パターンで実装すると、次のようなクラス図になる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| import java.util.*;
abstract class CState {
public int flyMiles; // マイル数
public abstract double travel(int miles, CFrequentFlyer context); // 累積マイルに基づいてランクを調整
}
class CNoCustomer extends CState { // 非会員
public double travel(int miles, CFrequentFlyer context) {
System.out.println("Your travel will not account for points");
return miles; // マイルは貯まらない
}
}
class CBasic extends CState { // 一般会員
public double travel(int miles, CFrequentFlyer context) {
if (context.flyMiles >= 25000 && context.flyMiles < 50000)
context.setState(new CSilver());
if (context.flyMiles >= 50000)
context.setState(new CGold());
return miles;
}
}
class CGold extends CState { // ゴールド会員
public double travel(int miles, CFrequentFlyer context) {
if (context.flyMiles >= 25000 && context.flyMiles < 50000)
context.setState(new CSilver());
if (context.flyMiles < 25000);
context.setState(new CBasic());
return miles + 0.5 * miles; // マイルにボーナスがつく
}
}
class CSilver extends CState { // シルバー会員
public double travel(int miles, CFrequentFlyer context) {
if (context.flyMiles <= 25000)
context.setState(new CBasic());
if (context.flyMiles >= 50000)
context.setState(new CGold());
return (miles + 0.25 * miles); // マイルにボーナスがつく
}
}
class CFrequentFlyer {
CState state;
double flyMiles;
public CFrequentFlyer() {
state = new CNoCustomer();
flyMiles = 0;
setState(state);
}
public void setState(CState state) { this.state = state; }
public void travel(int miles) {
double bonusMiles = state.travel(miles, this);
flyMiles = flyMiles + bonusMiles;
}
}
|
例 3
自動販売機、在庫ありと在庫なしの例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
| public class StatePattern {
public static void main(String[] args) {
Context context = new Context();
context.Request(); // count = 2
context.Request(); // count = 1
context.Request(); // count = 0
context.Request(); // switch to State A (在庫補充)
context.Request(); // count = 4
}
}
class Context{ // 自販機
private int count;
private State state;
public Context(){
count = 3;
state = new StateA();
}
public int getCount() {
return count;
}
public State getState() {
return state;
}
public void setCount(int count) {
this.count = count;
}
public void setState(State state) {
this.state = state;
}
public void Request(){ // 飲み物を買う
state.Handle(this);
}
}
interface State{
public void Handle(Context context);
}
class StateA implements State{ // 在庫あり
@Override
public void Handle(Context context){
int count = context.getCount();
if(count >= 1){
context.setCount(count - 1);
System.out.println("Complete! あと" + context.getCount() + "個");
if(context.getCount() == 0){
context.setState(new StateB());
}
}else{
System.out.println("Refused!");
}
}
}
class StateB implements State{ // 在庫なし
@Override
public void Handle(Context context){
int count = context.getCount();
if(count == 0){
System.out.println("Refused!");
context.setCount(5); // 在庫を5つ補充
System.out.println("Please try again");
context.setState(new StateA());
}else {
context.setState(new StateA());
context.Request();
}
}
}
|