デザインパターン Stateについて

Stateパターンとは

ステートパターンは状態をクラスとして表現して、クラスを切り替えることで状態の変化を表すことができます。

サンプルプログラム

状態をクラスとして表現する例として、ATM使用時に時刻ごとに手数料が異なるシステムを考えてみましょう。
以下を時刻ごとの手数料として、画面に表示するとします。

9:00~17:59
・手数料が0円

18:00~8:59
・手数料が200円

これを単純にif文で表現すると以下のようになります。

   ATM使用時に呼ばれるメソッド () {
       if (9:00~17:59) {
           手数料0円を画面表示
       } else if (18:00~8:59) {
           手数料200円を画面表示
       }
   }

Stateパターンを使わない場合は、状態のチェックにif文を使用します。
Stateパターンではif文を使用しません。
9:00~17:59と18:00~8:59のATMの状態をクラスとして表現します。
作成したサンプルプログラムを見てみましょう。

・サンプルプログラムで登場するクラスとインターフェース

f:id:xxyoko10:20200412230513p:plain

Stateインターフェース

ATMの状態を表しています。
引数のContextで状態の管理を行っており、
useATMメソッドは状態に応じて、処理が変化するメソッドです。

public interface State {
    public abstract void doClock(Context context, int hour);    // 時刻設定
    public abstract void useATM(Context context);               // ATM使用
}

DayState1クラス

DayState1クラスは9:00~17:59の状態を表すクラスです。
doClockメソッド内で、状態の遷移をしています。
また、インスタンスは一つしか作れないように、Singletonパターンを使用しています。

public class DayState1 implements State {
    private static DayState1 singleton = new DayState1();
    private DayState1() {                                // コンストラクタはprivate
    }
    public static State getInstance() {                 // 唯一のインスタンスを得る
        return singleton;
    }
    public void doClock(Context context, int hour) {    // 時刻設定
        if (hour < 9 || 18 <= hour) {
            context.changeState(DayState2.getInstance());
        }
    }
    public void useATM(Context context) {               // ATM使用
        context.showScreen("手数料は0円です。");
    }
}

DayState2クラス

DayState2クラスは18:00~8:59の状態を表すクラスです。
DayState1と同様にdoClockメソッド内で、状態の遷移をしています。

public class DayState2 implements State {
    private static DayState2 singleton = new DayState2();
    private DayState2() {                              // コンストラクタはprivate
    }
    public static State getInstance() {                 // 唯一のインスタンスを得る
        return singleton;
    }
    public void doClock(Context context, int hour) {    // 時刻設定
        if (9 <= hour && hour < 18) {
            context.changeState(DayState1.getInstance());
        }
    }
    public void useATM(Context context) {               // ATM使用
        context.showScreen("手数料は200円です。");
    }
}

Contextクラス

状態の管理をしているインターフェースです。

public interface Context {

    public abstract void setClock(int hour);                // 時刻設定
    public abstract void changeState(State state);          // 状態変化
    public abstract void showScreen(String msg);            // 手数料表示
    public abstract void recordLog(String msg);             // 記録
}

ATMFrameクラス

GUIを使ってATMシステムを実現するクラスです。
コンストラクタで背景色やボタンの配置を決めています。
ここで注目すべきは、ATM使用ボタンを押したときに呼ばれる、useATMメソッドです。
if文で時刻の状態チェックをするわけでなく、いきなりメソッドを呼び出しています。
ここでは現在の状態を表すクラスをchangeStateメソッドに引数で渡し、
状態を表すフィールドに設定することで、遷移を行っています。

public class ATMFrame extends Frame implements ActionListener, Context {
    private TextField textClock = new TextField(60);        // 現在時刻表示
    private TextArea textScreen = new TextArea(10, 60);     // ATM出力
    private Button buttonUse = new Button("ATM使用");       // ATM使用ボタン
    private Button buttonExit = new Button("終了");         // 終了ボタン

    private State state = DayState1.getInstance();           // 現在の状態

    // コンストラクタ
    public ATMFrame(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(buttonExit);
        // そのパネルを配置
        add(panel, BorderLayout.SOUTH);
        // 表示
        pack();
        show();
        // リスナーの設定
        buttonUse.addActionListener(this);
        buttonExit.addActionListener(this);
    }
    // ボタンが押されたらここに来る
    public void actionPerformed(ActionEvent e) {
        System.out.println(e.toString());
        if (e.getSource() == buttonUse) {              // ATM使用ボタン
            state.useATM(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);
    }
    // 状態変化
    public void changeState(State state) {
        System.out.println(this.state + "から" + state + "へ状態が変化しました。");
        this.state = state;
    }
    // ATMの画面に手数料を出力
    public void showScreen(String msg) {
        textScreen.append(msg + "\n");
    }
    // ATM記録
    public void recordLog(String msg) {
        textScreen.append("record ... " + msg + "\n");
    }
}

Mainクラス

時刻の設定を1時間ごとに行っています。

public class Main {
    public static void main(String[] args) {
        ATMFrame frame = new ATMFrame("State Sample");
        while (true) {
            for (int hour = 0; hour < 24; hour++) {
                frame.setClock(hour);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
            }
        }
    }
}

実行結果

18:00~8:59にATM使用ボタン押下 f:id:xxyoko10:20200413003324p:plain

9:00~17:59にATM使用ボタン押下 f:id:xxyoko10:20200413003430p:plain

まとめ

システムの各状態を個別のクラスで表現するStateパターンを学びました。
Stateパターンで押さえておくべきポイントは以下の2点です。

・抽象メソッドとして宣言し、インターフェースとする
・具象メソッドとして実装し、個々のクラスとする

これがStateパターンの状態に依存した処理の表現方法となります。