【Java】State パターン【デザインパターン】
目次
Stateパターンについて
Stateパターンとは
- Stateパターンは、状態をクラスとして表現し、クラスを切り替えることによって「状態の変化」を表すパターンです。
- 状態をクラスとして表せば、クラスを切り替えることで、「状態の変化」が表現でき、新しい状態を追加しやすくなります
サンプルプログラム
- Stateパターンを使って、昼、夜の状態によって警備の状態が変わる、金庫管理のプログラム例を取り上げます
- 出来事のメソッドの中のif文で、状態(昼間/夜間)を調べ、処理を変更するではなく、昼間/夜間、という状態を表すクラスを用意して、出来事に応じたメソッドを定義します
- こうすることで、状態確認のためのif文は不要になります
- 各クラスの役割は以下のようになっています
名前 | 役割 |
---|---|
Context | 金庫の状態変化を管理し、警備センターとの連絡をとるインターフェース |
SafeFrame | Contextインターフェース実装クラス。ボタンや画面表示などのUIを持つ。 |
State | 金庫の状態を表すインターフェース |
DayState(ConcreteState) | Stateインターフェース実装クラス(昼間の状態) |
NightStateクラス(ConcreteState) | Stateインターフェース実装クラス(夜間の状態) |
Stateインターフェース
- 金庫の状態を表すインターフェースです
- 出来事に対応して呼び出されるインターフェースを規定します
- ここで規定されているメソッドは、状態に応じて処理が変化する、状態依存のメソッドの集合です
public interface State { public abstract void doClock(Context context, int hour); // 時刻設定 public abstract void doUse(Context context); // 金庫使用 public abstract void doAlarm(Context context); // 非常ベル public abstract void doPhone(Context context); // 通常通話 }
DayStateクラス
- 昼間の状態を表すクラスです
- 状態を表すクラスでは、メモリを節約するために、一個ずつしかインスタンスを生成しないようにします(Singletonパターン)
- doClockメソッドの中で、時刻に応じて状態の遷移が起こります
public class DayState implements State { private static DayState singleton = new DayState(); private DayState() { // コンストラクタはprivate } public static State getInstance() { // 唯一のインスタンスを得る return singleton; } public void doClock(Context context, int hour) { // 時刻設定 if (hour < 9 || 17 <= hour) { context.changeState(NightState.getInstance()); //状態遷移 } } public void doUse(Context context) { // 金庫使用 context.recordLog("金庫使用(昼間)"); } public void doAlarm(Context context) { // 非常ベル context.callSecurityCenter("非常ベル(昼間)"); } public void doPhone(Context context) { // 通常通話 context.callSecurityCenter("通常の通話(昼間)"); } public String toString() { // 文字列表現 return "[昼間]"; } }
NightStateクラス
- 夜間の状態を表すクラスです
- Singletonパターンを使います
public class NightState implements State { private static NightState singleton = new NightState(); private NightState() { // コンストラクタはprivate } public static State getInstance() { // 唯一のインスタンスを得る return singleton; } public void doClock(Context context, int hour) { // 時刻設定 if (9 <= hour && hour < 17) { context.changeState(DayState.getInstance()); } } public void doUse(Context context) { // 金庫使用 context.callSecurityCenter("非常:夜間の金庫使用!"); } public void doAlarm(Context context) { // 非常ベル context.callSecurityCenter("非常ベル(夜間)"); } public void doPhone(Context context) { // 通常通話 context.recordLog("夜間の通話録音"); } public String toString() { // 文字列表現 return "[夜間]"; } }
Contextインターフェース
- 状態を管理したり、警備センターの呼び出しを行います
public interface Context { public abstract void setClock(int hour); // 時刻の設定 public abstract void changeState(State state); // 状態変化 public abstract void callSecurityCenter(String msg); // 警備センター警備員呼び出し public abstract void recordLog(String msg); // 警備センター記録 }
SafeFrameクラス
- GUIを使って金庫管理システムを実現します
- stateフィールドだけ、金庫の現在の情報を表し、昼間の状態で初期化されます
- ボタンが押されたら、状態を調べることなく、
state.doUse(this);
を呼びます - doClockメソッドでchangeStateを呼び出します
this.state = state;
で、現在の状態を表しているフィールドに、状態を表すクラスのインスタンスを代入することが、状態遷移に相当します
import java.awt.Frame; import java.awt.Label; import java.awt.Color; import java.awt.Button; import java.awt.TextField; import java.awt.TextArea; import java.awt.Panel; import java.awt.BorderLayout; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; public class SafeFrame extends Frame implements ActionListener, Context { private TextField textClock = new TextField(60); // 現在時刻表示 private TextArea textScreen = new TextArea(10, 60); // 警備センター出力 private Button buttonUse = new Button("金庫使用"); // 金庫使用ボタン private Button buttonAlarm = new Button("非常ベル"); // 非常ベルボタン private Button buttonPhone = new Button("通常通話"); // 通常通話ボタン private Button buttonExit = new Button("終了"); // 終了ボタン private State state = DayState.getInstance(); // 現在の状態 // コンストラクタ public SafeFrame(String title) { super(title); setBackground(Color.lightGray); setLayout(new BorderLayout()); // textClockを配置 add(textClock, BorderLayout.NORTH); textClock.setEditable(false); // textScreenを配置 add(textScreen, BorderLayout.CENTER); textScreen.setEditable(false); // パネルにボタンを格納 Panel panel = new Panel(); panel.add(buttonUse); panel.add(buttonAlarm); panel.add(buttonPhone); panel.add(buttonExit); // そのパネルを配置 add(panel, BorderLayout.SOUTH); // 表示 pack(); show(); // リスナーの設定 buttonUse.addActionListener(this); buttonAlarm.addActionListener(this); buttonPhone.addActionListener(this); buttonExit.addActionListener(this); } // ボタンが押されたらここに来る public void actionPerformed(ActionEvent e) { System.out.println(e.toString()); if (e.getSource() == buttonUse) { // 金庫使用ボタン state.doUse(this); } else if (e.getSource() == buttonAlarm) { // 非常ベルボタン state.doAlarm(this); } else if (e.getSource() == buttonPhone) { // 通常通話ボタン state.doPhone(this); } else if (e.getSource() == buttonExit) { // 終了ボタン System.exit(0); } else { System.out.println("?"); } } // 時刻の設定 public void setClock(int hour) { String clockstring = "現在時刻は"; if (hour < 10) { clockstring += "0" + hour + ":00"; } else { clockstring += hour + ":00"; } System.out.println(clockstring); textClock.setText(clockstring); state.doClock(this, hour); //doClockでchangeStateを呼び出します } // 状態変化 public void changeState(State state) { System.out.println(this.state + "から" + state + "へ状態が変化しました。"); this.state = state; //状態を変化 } // 警備センター警備員呼び出し public void callSecurityCenter(String msg) { textScreen.append("call! " + msg + "\n"); } // 警備センター記録 public void recordLog(String msg) { textScreen.append("record ... " + msg + "\n"); } }
Mainクラスで動作確認
public class Main { public static void main(String[] args) { SafeFrame frame = new SafeFrame("State Sample"); while (true) { for (int hour = 0; hour < 24; hour++) { frame.setClock(hour); // 時刻の設定 try { Thread.sleep(1000); } catch (InterruptedException e) { } } } } }
Stateパターンのメリット
- 分割して統合せよ(divide and conquer)という方針は、複雑で大規模なプログラミングによく登場します。
- 大きくてややこしい問題を解くときは、小さい問題に分けましょう、という方針です。
- Stateパターンでは、個々の具体的な状態を、別々のクラスとして表現して、複雑な問題を分割しています
- DayStateを実装している最中、プログラマは他のクラス(NightState)を意識する必要はなくなります。
- 状態の数がもっと増えた場合に、Stateパターンは一層強みを発揮します。
- Stateインターフェースのメソッドは、全て"状態に依存した処理"になっています
- Stateパターンでは、状態に依存した処理を、以下のように表現しています
- 抽象メソッドで宣言し、インターフェースにする
- 具象メソッドで実装し、個々のクラスにする
- 新しい状態を追加するには、Stateインターフェースを実装したクラスを用意し、必要なメソッドを書けばOKです
今日のポイント
- Stateパターンは、状態をクラスとして表現し、クラスを切り替えることによって「状態の変化」を表すパターンです。
- 状態をクラスとして表せば、クラスを切り替えることで、「状態の変化」が表現でき、新しい状態を追加しやすくなります
- Stateパターンでは、個々の具体的な状態を、別々のクラスとして表現して、複雑な問題を分割しています(divide and conquer)
- Stateパターンでは、状態に依存した処理を、"抽象メソッドで宣言しインターフェースにして、それを具象メソッドとして実装し個々のクラスにする" ことで、表現しています
本日もお疲れ様です😊