技術とか戦略とか

IT技術者が技術や戦略について書くブログです。

javaでのStateパターン

Stateパターンとはデザインパターンの一種で、1つ1つの状態をそのままクラスとして表現するパターンです。
通常のコーディングだとIF文で状態毎の処理を記述させるためそれぞれの状態でどのような処理が行われるのか読み取りにくいのですが、このパターンを適用すれば状態遷移図上の状態とクラスが1対1で結びつくようになるので、処理が単純明快になります。
 
今回は、朝・昼・夜の各状態で出現モンスターが変わるサンプルコードを作成してみました。
このパターンは記述内容としては決して難しくないので、サンプルコードを見た方が理解が早いと思います。
 
【サンプルコード】
・StateEnum.java
// Stateの番号や共通処理を定義するEnumクラス
//
// 今回のデザインパターンと直接関係ありませんが、
// 共通の定数を使用するため定義します。
public enum StateEnum {
    unknown(0),
    morning(1),
    day(2),
    night(3);

    private int id;

    private StateEnum (int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    public static StateEnum valueOf(int id) {
        StateEnum[] array = values();
        for(StateEnum num : array) {
            if (id == num.getId()){
                return num;
            }
        }
        return null;
    }

    public static int stateValidateJudge(int id) {
        if (id < 0 || id > 3) {
            System.out.println("不明なStateです。UnknownStateと仮定します。");
            return 0;
        } else {
            return id;
        }
    }

    public static int stateChangeJudge(int hour) {
        if (hour < 0 || hour > 23) {
            System.out.println("不正な時刻です。");
            return -1;
        } else if (hour >= 5 && hour < 10) {
            return morning.getId();
        } else if (hour >= 10 && hour < 18) {
            return day.getId();
        } else {
            return night.getId();
        }
    }

}

・State.java
// State毎に作成するクラス
// 本来のStateパターンではインターフェースですが、
// 今回は共通処理が存在するため、抽象クラスで定義します。
public abstract class State {

    protected int stateId;

    public State(int stateId) {
        this.stateId = StateEnum.stateValidateJudge(stateId);
    }

    // Stateの変更判定だけする形にします。
    // 本来のStateパターンではどのオブジェクトに変更するかも定義しますが、
    // 密結合を避けるため、今回は利用者側にそれを判断させる形にします。
    public int stateChangeJudge(int hour) {
        int futureStateId = StateEnum.stateChangeJudge(hour);
        if (stateId == futureStateId) {
            return 0; // 変更無し
        } else {
            return futureStateId;
        }
    };

    public abstract void stateMethod(Context context);

}

・UnknownState.java
// 1回も状態遷移をしていない場合のState
// 通常使わない想定です。
public class UnknownState extends State{

    private static State unknownState = new UnknownState();

    private UnknownState() {
        super(StateEnum.unknown.getId());
    }

    // 複数のインスタンスを用意する必要がないのでシングルトン
    public static State getInstance() {
        return unknownState;
    }

    public void stateMethod(Context context) {
        context.contextMethod("まおう");
    }

}

・MorningState.java
// 朝の場合のState
public class MorningState extends State{

    private static State morningState = new MorningState();

    private MorningState() {
        super(StateEnum.morning.getId());
    }

    // 複数のインスタンスを用意する必要がないのでシングルトン
    public static State getInstance() {
        return morningState;
    }

    public void stateMethod(Context context) {
        context.contextMethod("とりモンスター");
    }

}

・DayState.java
// 昼の場合のState
public class DayState extends State{

    private static State dayState = new DayState();

    private DayState() {
        super(StateEnum.day.getId());
    }

    // 複数のインスタンスを用意する必要がないのでシングルトン
    public static State getInstance() {
        return dayState;
    }

    public void stateMethod(Context context) {
        context.contextMethod("いぬモンスター");
        context.contextMethod("ねこモンスター");
    }

}

・NightState.java
// 夜の場合のState
public class NightState extends State{

    private static State nightState = new NightState();

    private NightState() {
        super(StateEnum.night.getId());
    }

    // 複数のインスタンスを用意する必要がないのでシングルトン
    public static State getInstance() {
        return nightState;
    }

    public void stateMethod(Context context) {
        context.contextMethod("こうもりモンスター");
    }

}

・Context.java
// Stateの管理を行うクラス
public class Context {

    private State state = null;

    public Context() {
        state = UnknownState.getInstance();
    }

    public void setState(State state) {
        this.state = state;
    }

    public void stateChangeJudge(int hour) {
        int futureStateId = state.stateChangeJudge(hour);
        switch (futureStateId){
          case 1:
            setState(MorningState.getInstance());
            break;
          case 2:
            setState(DayState.getInstance());
            break;
          case 3:
            setState(NightState.getInstance());
            break;
          case -1:
            System.out.println("不正な操作のため状態変更はしません。");
            break;
          default: // 0が返る場合。状態変更無し。
            break;
        }

    }

    public void stateMethod() {
        state.stateMethod(this);
    }

    public void contextMethod(String monster) {
        System.out.println(monster + " があらわれた!");
    }

}

・StateMain.java
// メインクラス
public class StateMain {
    public static void main(String[ ] args) {
        Context context = new Context();
        for (int i = 0; i < 24; i++) {
            System.out.println("現在 " + i + "時");
            context.stateChangeJudge(i);
            context.stateMethod();
            System.out.println();
        }
    }
}

【実行結果】
現在 0時
こうもりモンスター があらわれた!

現在 1時
こうもりモンスター があらわれた!

現在 2時
こうもりモンスター があらわれた!

現在 3時
こうもりモンスター があらわれた!

現在 4時
こうもりモンスター があらわれた!

現在 5時
とりモンスター があらわれた!

現在 6時
とりモンスター があらわれた!

現在 7時
とりモンスター があらわれた!

現在 8時
とりモンスター があらわれた!

現在 9時
とりモンスター があらわれた!

現在 10時
いぬモンスター があらわれた!
ねこモンスター があらわれた!

現在 11時
いぬモンスター があらわれた!
ねこモンスター があらわれた!

現在 12時
いぬモンスター があらわれた!
ねこモンスター があらわれた!

現在 13時
いぬモンスター があらわれた!
ねこモンスター があらわれた!

現在 14時
いぬモンスター があらわれた!
ねこモンスター があらわれた!

現在 15時
いぬモンスター があらわれた!
ねこモンスター があらわれた!

現在 16時
いぬモンスター があらわれた!
ねこモンスター があらわれた!

現在 17時
いぬモンスター があらわれた!
ねこモンスター があらわれた!

現在 18時
こうもりモンスター があらわれた!

現在 19時
こうもりモンスター があらわれた!

現在 20時
こうもりモンスター があらわれた!

現在 21時
こうもりモンスター があらわれた!

現在 22時
こうもりモンスター があらわれた!

現在 23時
こうもりモンスター があらわれた!