mohuneko’s blog

かんばる駆け出しエンジニアのブログです

【Java】State パターン【デザインパターン】

駆け出しエンジニアがデザインパターンをもくもく勉強します

 こんな本で勉強しています🌟

目次

Stateパターンについて

Stateパターンとは

  • Stateパターンは、状態をクラスとして表現し、クラスを切り替えることによって「状態の変化」を表すパターンです。
  • 状態をクラスとして表せば、クラスを切り替えることで、「状態の変化」が表現でき、新しい状態を追加しやすくなります

サンプルプログラム

  • Stateパターンを使って、昼、夜の状態によって警備の状態が変わる、金庫管理のプログラム例を取り上げます
  • 出来事のメソッドの中のif文で、状態(昼間/夜間)を調べ、処理を変更するではなく、昼間/夜間、という状態を表すクラスを用意して、出来事に応じたメソッドを定義します
    • こうすることで、状態確認のためのif文は不要になります
  • 各クラスの役割は以下のようになっています
名前 役割
Context 金庫の状態変化を管理し、警備センターとの連絡をとるインターフェース
SafeFrame Contextインターフェース実装クラス。ボタンや画面表示などのUIを持つ。
State 金庫の状態を表すインターフェース
DayState(ConcreteState) Stateインターフェース実装クラス(昼間の状態)
NightStateクラス(ConcreteState) Stateインターフェース実装クラス(夜間の状態)

f:id:mohuNeko:20210102122553p:plain

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パターンでは、状態に依存した処理を、"抽象メソッドで宣言しインターフェースにして、それを具象メソッドとして実装し個々のクラスにする" ことで、表現しています

 本日もお疲れ様です😊