daisukeの技術ブログ

AI、機械学習、最適化、Pythonなどについて、技術調査、技術書の理解した内容、ソフトウェア/ツール作成について書いていきます

Javaでデザインパターンを学ぶ:Stateパターン

Javaによるデザインパターンの6回目です!

今回も教材は、参考文献の「Java言語で学ぶデザインパターン入門第3版」のサンプルコードを使わせて頂きます。

今回は、Stateパターンです。状態遷移を表現するのに便利なパターンで、理解しやすいパターンだと思います。

では、やっていきましょう!

参考文献

参考文献のサンプルプログラムのダウンロード

www.hyuki.com

はじめに

「Javaでデザインパターンを学ぶ」の記事一覧です。良かったら参考にしてください。

Javaでデザインパターンの記事一覧

先に、23種類のデザインパターンを示します。

参考サイト:デザインパターン (ソフトウェア) - Wikipedia

デザインパターン一覧
◆生成に関するパターン
 ・Abstract Factory:関連するインスタンスを状況に応じて、適切に生成する方法を提供する
 ・Builder:複合化されたインスタンスの生成過程を隠蔽する。
 ・Factory Method:生成されるインスタンスに依存しない、インスタンスの生成方法を提供する
 ・Prototype:同様のインスタンスを生成するために、原型のインスタンスを複製する
 ・Singleton:あるクラスについて、インスタンスが1つしか存在しないことを保証する
◆構造に関するパターン
 ・Adapter:元々関連性のない2つのクラスを接続するクラスを作る
 ・Bridge:クラスと呼び出し側の間の橋渡しをするクラスで、実装を隠蔽する
 ・Composite:再帰的な構造を表現する
 ・Decorator:あるインスタンスに対し、動的に付加機能を追加する
 ・Facade:複数のサブシステムの窓口となる共通のインタフェースを提供する
 ・Flyweight:多数のインスタンスを共有し、インスタンスの構築のための負荷を減らす
 ・Proxy:共通のインタフェースを持つインスタンスを内包し、利用者からのアクセスを代理する。Wrapperとも呼ばれる
◆振る舞いに関するパターン
 ・Chain of Responsibility:イベントの送受信を行う複数のオブジェクトを鎖状につなぎ、それらの間をイベントが渡されていくようにする
 ・Command:複数の異なる操作について、それぞれに対応するオブジェクトを用意し、オブジェクトを切り替えることで、操作の切り替えを実現する
 ・Interpreter:構文解析のために、文法規則を反映するクラス構造を作る
 ・Iterator:複数の要素を内包するオブジェクトのすべての要素に対して、順番にアクセスする方法を提供する
 ・Mediator:オブジェクト間の相互作用を仲介するオブジェクトを定義し、オブジェクト間の結合度を低くする
 ・Memento:データ構造に対する一連の操作のそれぞれを記録しておき、以前の状態の復帰または操作の再現が行えるようにする
 ・Observer:インスタンスの変化を他のインスタンスから監視できるようにする
 ・State:オブジェクトの状態を変化させることで、処理内容を変えられるようにする
 ・Strategy:データ構造に対して適用する一連のアルゴリズムをカプセル化し、アルゴリズムの切り替えを容易にする
 ・Template Method:アルゴリズムは抽象クラスで、処理内容はサブクラスで定義する
 ・Visitor:データ構造を保持するクラスと、それに対して処理を行うクラスを分離する

今回は、「Stateパターン」をやっていきます!

サンプルコードの理解

Stateパターンは、状態があり、その状態に応じて振る舞いが変わるような場合に使えます。

今回のサンプルコードの場合は、時間の概念があり、1時間ずつ経過していきます。時間とともに昼間と夜間と変化し、それぞれで振る舞いが変わる内容になっています。

昼間の場合はこういう振る舞いをして、夜間の場合はこういう振る舞いをする。という実装は、if文による分岐で実現される場合が多いと思いますが、Stateパターンを使うと、昼間と夜間でそれぞれに振る舞いを実装しておき、Stateによって振る舞いが変わる仕組みになっています。

では、実際に、サンプルコードを見ていくことにします。

今回のPlantUMLのクラス図

PlantUMLで作成したクラス図のコードと画像ファイルです。

@startuml
interface State {
    + {abstract} void doClock()
    + {abstract} void doUse()
    + {abstract} void doAlarm()
    + {abstract} void doPhone()
}

class DayState {
    - {static} DayState singleton
    + DayState()
    + {static} State getInstance()
    + void doClock()
    + void doUse()
    + void doAlarm()
    + void doPhone()
    
}
State <|.. DayState

class NightState {
    - {static} NightState singleton
    + NightState()
    + {static} State getInstance()
    + void doClock()
    + void doUse()
    + void doAlarm()
    + void doPhone()
    + String toString()
}
State <|.. NightState

interface Context {
    + {abstract} void setClock()
    + {abstract} void changeState()
    + {abstract} void callSecurityCenter()
    + {abstract} void recordLog()
}

class SafeFrame {
    - TextField textClock
    - TextArea textScreen
    - Button buttonUse
    - Button buttonAlarm
    - Button buttonPhone
    - Button buttonExit
    - State state
    + SafeFrame()
    + void actionPerformed()
    + void setClock()
    + void changeState()
    + void callSecurityCenter()
    + void recordLog()
}
Context <|.. SafeFrame
SafeFrame o- State
@enduml

今回のクラス図
今回のクラス図

Mainクラス(使う側のクラス)

まずは、Stateパターンが、どのように使われるのかを見るために、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) {
                }
            }
        }
    }
}

実行結果

実行結果
実行結果

今回のサンプルコードは、GUIのJavaプログラムでした。

SafeFrameクラスのインスタンスを作り、あとは無限ループです。

ループの中では、1秒に1回、1時間ずつ時間を進めているようです。実際にGUIのウィンドウの「現在時刻はxx:xx」という表示は1秒ごとに1時間ずつ進んでいました。

また、「金庫使用」、「非常ベル」、「通常通話」のボタンを押すと、ターミナルにそれぞれのログがボタンを押したタイミングで表示されていました。

もう少し中身を見ていきます。

Contextクラス

Contextクラスは、インターフェースです。

4つの抽象メソッドを定義しています。

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);             // 警備センター記録
}

次は、Contextクラスのインタフェースを実装しているクラスを見ていきます。

SafeFrameクラス

Contextクラスのインタフェースを実装したSafeFrameクラスです。

GUIの部分を実装しています。

プライベートメンバ変数として、GUI部品の時刻を表示しているテキストフィールド、ログを表示しているテキストエリア、金庫使用ボタン、非常ベルボタン、通常通話ボタン、終了ボタンを定義しています。

さらに、プライベートメンバ変数として、DayStateクラス(昼間の状態)のインスタンスを取得しています。DayStateクラスは後述しますが、シングルトンパターンを実装しています。

コンストラクタでは、GUI部品の初期化と、イベント(リスナー)の初期化を行っています。コメントが書かれているので、詳細の説明は省略させていただきます。

actionPerformed() は、イベントが発生したら呼ばれるメソッドで、4つのボタンが押されたときのそれぞれの行動を決めています。DayStateクラスのメソッドを呼び出しているようです。

あとは、Contextクラスで定義していたインタフェースを実装したメソッドが4つあります。

setClock() では、Mainクラスから時間が渡されて、それをGUI部品の現在時刻表示に反映して、DayStateクラスの doClock() を呼び出しています。

changeState() では、渡されたstateをプライベートメンバ変数に設定しています。最初は DayStateクラスのインスタンス(昼間の状態)を設定していましたが、時間が夜間になったら、このメソッドで、NightStateクラスのインスタンス(夜間の状態)を設定します。

callSecurityCenter() では、非常ベルボタンを押された場合に、Stateクラスの doAlarm() を呼び出し、そこから、このメソッドが呼ばれます。GUIのテキストエリアにメッセージを設定しています。

recordLog() は、callSecurityCenter() と同様で、通常通話ボタンが押されると、Stateクラスの doPhone() を呼び出し、そこから、このメソッドが呼ばれます。同じように、GUIのテキストエリアにメッセージを設定しています。

import java.awt.BorderLayout;
import java.awt.Button;
import java.awt.Color;
import java.awt.Frame;
import java.awt.Panel;
import java.awt.TextArea;
import java.awt.TextField;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

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();
        setVisible(true);
        // リスナーの設定
        buttonUse.addActionListener(this);
        buttonAlarm.addActionListener(this);
        buttonPhone.addActionListener(this);
        buttonExit.addActionListener(this);
    }

    // ボタンが押されたらここに来る
    @Override
    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("?");
        }
    }

    // 時刻の設定
    @Override
    public void setClock(int hour) {
        String clockstring = String.format("現在時刻は%02d:00", hour);
        System.out.println(clockstring);
        textClock.setText(clockstring);
        state.doClock(this, hour);
    }

    // 状態変化
    @Override
    public void changeState(State state) {
        System.out.println(this.state + "から" + state + "へ状態が変化しました。");
        this.state = state;
    }

    // 警備センター警備員呼び出し
    @Override
    public void callSecurityCenter(String msg) {
        textScreen.append("call! " + msg + "\n");
    }

    // 警備センター記録
    @Override
    public void recordLog(String msg) {
        textScreen.append("record ... " + msg + "\n");
    }
}

Stateクラス

Stateクラスは、インターフェースです。

4つの抽象メソッドを定義しています。

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クラス

Stateクラスのインタフェースを実装したDayStateクラスです。

DayStateクラス(昼間の状態)は、シングルトンのパターンを使っています。staticなプライベートメンバ変数として、自分自身のインスタンスを持ち、staticな getInstance でインスタンスを取得できます。

コンストラクタは何もしていないですね。

Stateクラスで定義されていた4つの具象メソッドを実装しています。

doClock() では、17時から23時、もしくは、0時から8時なら、Stateを NightState(夜間の状態)に変えています。このクラスと同様、NightStateクラスもシングルトンのパターンを使っていて、NightStateクラスの getInstance() で、NightStateクラスのインスタンスを取得することができます。

doUse() では、SafeFrameクラスの recordLog() を呼び出しています。先ほど見たように、SafeFrameクラスの recordLog() では、GUI部品のテキストエリアにメッセージを設定していました。

doAlarm() と、doPhone() も同様で、SafeFrameクラスのメソッドを呼び出していて、そこでGUI部品のテキストエリアにメッセージを設定しています。

最後に toString() は、インスタンスが文字列として使われる場合に呼ばれるメソッドです。"昼間"という文字列を返しています。

public class DayState implements State {
    private static DayState singleton = new DayState();

    private DayState() {
    }

    public static State getInstance() {
        return singleton;
    }

    @Override
    public void doClock(Context context, int hour) {
        if (hour < 9 || 17 <= hour) {
            context.changeState(NightState.getInstance());
        }
    }

    @Override
    public void doUse(Context context) {
        context.recordLog("金庫使用(昼間)");
    }

    @Override
    public void doAlarm(Context context) {
        context.callSecurityCenter("非常ベル(昼間)");
    }

    @Override
    public void doPhone(Context context) {
        context.callSecurityCenter("通常の通話(昼間)");
    }

    @Override
    public String toString() {
        return "[昼間]";
    }
}

NightStateクラス

Stateクラスのインタフェースを実装したNightStateクラスです。

DayStateクラスとほとんど同じなので、違いがあるところだけ説明します。

DayStateクラスの doClock() では、17時から23時、もしくは、0時から8時なら、Stateを NightState(夜間の状態)に変えていましたが、NightStateクラスの doClock() では、逆に、9時から16時の場合に、State を DayState(昼間の状態)に変えています。

あとは、値が異なるだけで、やっていることはDayStateクラスと同じです。

public class NightState implements State {
    private static NightState singleton = new NightState();

    private NightState() {
    }

    public static State getInstance() {
        return singleton;
    }

    @Override
    public void doClock(Context context, int hour) {
        if (9 <= hour && hour < 17) {
            context.changeState(DayState.getInstance());
        }
    }

    @Override
    public void doUse(Context context) {
        context.callSecurityCenter("非常:夜間の金庫使用!");
    }

    @Override
    public void doAlarm(Context context) {
        context.callSecurityCenter("非常ベル(夜間)");
    }

    @Override
    public void doPhone(Context context) {
        context.recordLog("夜間の通話録音");
    }

    @Override
    public String toString() {
        return "[夜間]";
    }
}

ソースコードの量が多かったので、Stateパターンについて、ここで考えてみたいと思います。

Stateを変化させると、インタフェースが同じ別のクラスのインスタンスのメソッドが呼ばれるところが、Stateパターンの特徴ですね。

状態が増えたとしても、それぞれのメソッドのif文の分岐を増やすのではなく、Stateクラスを継承したクラスを増やすことで対応ができます。複雑な状態遷移を持つシステムの場合でも、すっきりと実装できそうです。

おわりに

今回はStateパターンを学びました。

Stateパターンのサンプルコードは、やってることが明確で、分かりやすかったですね。

今回は以上です!

最後までお読みいただき、ありがとうございました。