広告

Java : State パターン (図解/デザインパターン)

State パターンとは、GoF によって定義されたデザインパターンの1つです。
オブジェクトの状態を「振る舞いを持たせたクラスオブジェクト」で表します。

本記事では、State パターンを Java のコード付きで解説していきます。


デザインパターン(GoF) 関連記事

概要

State パターン(英: state pattern、ステート・パターン)とは、プログラミングで用いられる振る舞いに関する(英語版) デザインパターンの一種である。このパターンはオブジェクトの状態(state)を表現するために用いられる。

State パターンとは、

  • オブジェクトの状態を「振る舞いを持たせたクラスオブジェクト」で表す
  • それにより if や switch などの条件式を減らす

という設計です。

State は、日本語的に発音すると「ステート」となります。
意味は「状態」ですね。


【State パターンのイメージ】

イメージ図1

状態を持った1つのクラスがあります。
このクラスを「コンテキスト」クラスとしましょう。

シンプルな状態であれば、数値 や 列挙値(enum) で表すことも多いと思います。

public enum State {
    A,
    B,
    C,
}

public class コンテキスト {
    private State state;

    ...
}

しかし、「コンテキスト」クラスの各メソッドで、

  • 状態ごとに処理を変えたい

そんな場合はどうでしょうか?

おそらく、それぞれのメソッドで if や switch などの条件式が必要になるでしょう。

public class コンテキスト {
    private State state;

    public void func1() {
        switch (state) {
            case A -> {
                ...
            }
            case B -> {
                ...
            }
            case C -> {
                ...
            }
        }
    }

    public void func2() {
        switch (state) {
            case A -> {
                ...
            }
            case B -> {
                ...
            }
            case C -> {
                ...
            }
        }
    }

    ...
}

上記の例くらいならいいですが、ここからさらに …

  • メソッドが増える
  • 状態が増える

なんてことになると、条件分岐も増え、処理も複雑になっていくでしょう。
コードの保守も大変になります。

これらの問題を解決するのが State パターンです。

State パターンでは、状態をクラスオブジェクトで表します。
そして、状態ごとの振る舞いは State インタフェースをそれぞれ実装して実現します。

public interface State {
    void func1();

    void func2();
}

public class A implements State {
    ... 状態Aの振る舞い ...
}

public class B implements State {
    ... 状態Bの振る舞い ...
}

public class C implements State {
    ... 状態Cの振る舞い ...
}

「コンテキスト」クラスでは、State オブジェクトに処理を 委譲 するだけです。

public class コンテキスト {
    private State state;

    public void func1() {
        state.func1();
    }

    public void func2() {
        state.func2();
    }

    ...
}

if や switch による条件式もなくなり、だいぶすっきりしました。
もし状態を切り替えたい場合は、state フィールドのオブジェクトを切り替えれば OK です。

状態を切り替える、という意味では State パターン は Strategy パターン の応用ともいえるでしょう。
そちらの記事も、ぜひ参考にしていただけたら幸いです。

State パターンのメリットまとめ

  • if や switch などの条件式がコードから減らせます
    • 条件式が減るとコードもシンプルになり保守も楽になります
    • ユニットテストも作りやすくなるでしょう

関連記事:


クラス図とシーケンス図

クラス図1

シーケンス図1

上の図は、State パターンの一般的なクラス図とシーケンス図です。
Context オブジェクトの request メソッドを呼び出すたびに、

  • 状態A (ConcreteStateA) <---> 状態B (ConcreteStateB)

を切り替えます。

それでは、このクラス図をそのままコードにしてみましょう。
まずは、State インタフェースとその実装クラスです。

public interface State {
    void handle();
}

public class ConcreteStateA implements State {
    private final Context context;

    public ConcreteStateA(Context context) {
        this.context = context;
    }

    @Override
    public void handle() {
        System.out.println("-- handle --");
        System.out.println("状態 : A");

        context.changeState(new ConcreteStateB(context));
    }
}

public class ConcreteStateB implements State {
    private final Context context;

    public ConcreteStateB(Context context) {
        this.context = context;
    }

    @Override
    public void handle() {
        System.out.println("-- handle --");
        System.out.println("状態 : B");

        context.changeState(new ConcreteStateA(context));
    }
}

Cotext.changeState メソッドを使って状態を切り替えるのがポイントですね。

次に、Context クラスです。
最初は「状態A」にしています。

public class Context {
    private State state = new ConcreteStateA(this);

    public void request() {
        state.handle();
    }

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

それでは、これらのクラスを使ってみましょう。

final var context = new Context();
context.request();

// 結果
// ↓
//-- handle --
//状態 : A

context.request();

// 結果
// ↓
//-- handle --
//状態 : B

context.request();

// 結果
// ↓
//-- handle --
//状態 : A

結果も問題なしですね。
Context オブジェクトの request メソッドを呼び出すたびに「状態A」と「状態B」が切り替わるようになりました。

【補足】

Context クラスの changeState メソッドは public として宣言しています。

しかし、changeState メソッドを公開したいのは State 側のクラスに対してのみです。
Context を使う側 (Client) には公開する必要はありません。

もし、そのあたりが気になるのであれば、インタフェースを分けるなどして工夫してみるのもよいかもですね。


具体的な例

もう少し具体的な例も見てみましょう。
動画を再生する VideoPlayer クラスを考えてみます。

クラス図4

VideoPlayer クラスには次のメソッドを用意します。

  • start : 動作の再生を開始
  • stop : 動画の再生を停止
  • pause : 動画の再生を一時停止
  • resume : 一時停止していた動画を再開
  • getState : 現在の状態を文字列で取得

そして、この VideoPlayer クラスは、次の状態遷移をとります。

状態遷移図

State パターンを使わないケース

まずは、State パターンを使わないでコードを書いてみましょう。
状態は 列挙値(enum) で表します。

クラス図2

public enum State {
    STOPPED,
    STARTED,
    PAUSED,
}

次に VideoPlayer クラスです。
各メソッドで switch による条件式があり、見るからに複雑そうですね …

public class VideoPlayer {
    private State state = State.STOPPED;

    public void start() {
        switch (state) {
            case STOPPED -> {
                System.out.println("再生開始 : OK");
                state = State.STARTED;
            }
            case STARTED -> {
                System.out.println("警告 : すでに再生中です。");
            }
            case PAUSED -> {
                System.out.println("警告 : 一時停止は resume で再開してください。");
            }
        }
    }

    public void stop() {
        switch (state) {
            case STOPPED -> {
                System.out.println("警告 : すでに停止中です。");
            }
            case STARTED -> {
                System.out.println("再生停止 : OK");
                state = State.STOPPED;
            }
            case PAUSED -> {
                System.out.println("警告 : 再生中ではありません。");
            }
        }
    }

    public void pause() {
        switch (state) {
            case STOPPED -> {
                System.out.println("警告 : 再生中ではありません。");
            }
            case STARTED -> {
                System.out.println("一時停止 : OK");
                state = State.PAUSED;
            }
            case PAUSED -> {
                System.out.println("警告 : すでに一時停止中です。");
            }
        }
    }

    public void resume() {
        switch (state) {
            case STOPPED, STARTED -> {
                System.out.println("警告 : 一時停止中ではありません。");
            }
            case PAUSED -> {
                System.out.println("再開 : OK");
                state = State.STARTED;
            }
        }
    }

    public String getState() {
        return switch (state) {
            case STOPPED -> {
                yield "停止中";
            }
            case STARTED -> {
                yield "再生中";
            }
            case PAUSED -> {
                yield "一時停止中";
            }
        };
    }
}

それでは、VideoPlayer クラスを使ってみましょう。
まずは正常に状態遷移するケースです。

final var player = new VideoPlayer();

System.out.println("-- start --");
System.out.println("状態(before) : " + player.getState());

player.start();
System.out.println("状態(after)  : " + player.getState());

// 結果
// ↓
//-- start --
//状態(before) : 停止中
//再生開始 : OK
//状態(after)  : 再生中

System.out.println("-- pause --");
System.out.println("状態(before) : " + player.getState());

player.pause();
System.out.println("状態(after)  : " + player.getState());

// 結果
// ↓
//-- pause --
//状態(before) : 再生中
//一時停止 : OK
//状態(after)  : 一時停止中

System.out.println("-- resume --");
System.out.println("状態(before) : " + player.getState());

player.resume();
System.out.println("状態(after)  : " + player.getState());

// 結果
// ↓
//-- resume --
//状態(before) : 一時停止中
//再開 : OK
//状態(after)  : 再生中

System.out.println("-- stop --");
System.out.println("状態(before) : " + player.getState());

player.stop();
System.out.println("状態(after)  : " + player.getState());

// 結果
// ↓
//-- stop --
//状態(before) : 再生中
//再生停止 : OK
//状態(after)  : 停止中

次に、状態遷移に失敗するケースです。
失敗した場合は、警告メッセージを出力します。

final var player = new VideoPlayer();

System.out.println("-- stop --");
System.out.println("状態 : " + player.getState());
player.stop();

// 結果
// ↓
//-- stop --
//状態 : 停止中
//警告 : すでに停止中です。

System.out.println("-- start --");
player.start();

// 結果
// ↓
//-- start --
//再生開始 : OK

System.out.println("-- start --");
System.out.println("状態 : " + player.getState());
player.start();

// 結果
// ↓
//-- start --
//状態 : 再生中
//警告 : すでに再生中です。

結果も問題なしですね。

しかし、この VideoPlayer クラスのコードを保守するのは、switch による条件式が多くてちょっと嫌ですよね …
もし新規の状態を追加することになったら、各メソッドのすべての switch 式 に手を入れる必要があります。

これは、開放閉鎖の原則

  • ソフトウェア要素(クラス、モジュール、関数など)は、拡張に対しては開いており、修正に対しては閉じているべきである。

にも反しています。


State パターンを使うケース

それでは State パターンを使ってコードを改善してみましょう。

クラス図3

State インターフェースでは、状態ごとに振る舞いを変えるメソッドを宣言します。

public interface State {
    void start();

    void stop();

    void pause();

    void resume();
}

State インターフェースを実装するクラスでは、それぞれの状態ごとに必要な処理を書きます。
また、Player.changeState メソッドを使って状態を遷移させます。

public class StoppedState implements State {
    private final VideoPlayer player;

    public StoppedState(VideoPlayer player) {
        this.player = player;
    }

    @Override
    public void start() {
        System.out.println("再生開始 : OK");
        player.changeState(new StartedState(player));
    }

    @Override
    public void stop() {
        System.out.println("警告 : すでに停止中です。");
    }

    @Override
    public void pause() {
        System.out.println("警告 : 再生中ではありません。");
    }

    @Override
    public void resume() {
        System.out.println("警告 : 一時停止中ではありません。");
    }

    @Override
    public String toString() {
        return "停止中";
    }
}

public class StartedState implements State {
    private final VideoPlayer player;

    public StartedState(VideoPlayer player) {
        this.player = player;
    }

    @Override
    public void start() {
        System.out.println("警告 : すでに再生中です。");
    }

    @Override
    public void stop() {
        System.out.println("再生停止 : OK");
        player.changeState(new StoppedState(player));
    }

    @Override
    public void pause() {
        System.out.println("一時停止 : OK");
        player.changeState(new PausedState(player));
    }

    @Override
    public void resume() {
        System.out.println("警告 : 一時停止中ではありません。");
    }

    @Override
    public String toString() {
        return "再生中";
    }
}

public class PausedState implements State {
    private final VideoPlayer player;

    public PausedState(VideoPlayer player) {
        this.player = player;
    }

    @Override
    public void start() {
        System.out.println("警告 : 一時停止は resume で再開してください。");
    }

    @Override
    public void stop() {
        System.out.println("警告 : 再生中ではありません。");
    }

    @Override
    public void pause() {
        System.out.println("警告 : すでに一時停止中です。");
    }

    @Override
    public void resume() {
        System.out.println("再開 : OK");
        player.changeState(new StartedState(player));
    }

    @Override
    public String toString() {
        return "一時停止中";
    }
}

最後に VideoPlayer クラスです。
基本的には State オブジェクトに処理を 委譲 するだけです。

public class VideoPlayer {
    private State state = new StoppedState(this);

    public void start() {
        state.start();
    }

    public void stop() {
        state.stop();
    }

    public void pause() {
        state.pause();
    }

    public void resume() {
        state.resume();
    }

    public String getState() {
        return state.toString();
    }

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

それでは、VideoPlayer クラスを使ってみましょう。
(State パターンを使わないケース とまったく同じ結果なので、読み飛ばしていただいても問題ありません)

まずは正常に状態遷移するケースです。

final var player = new VideoPlayer();

System.out.println("-- start --");
System.out.println("状態(before) : " + player.getState());

player.start();
System.out.println("状態(after)  : " + player.getState());

// 結果
// ↓
//-- start --
//状態(before) : 停止中
//再生開始 : OK
//状態(after)  : 再生中

System.out.println("-- pause --");
System.out.println("状態(before) : " + player.getState());

player.pause();
System.out.println("状態(after)  : " + player.getState());

// 結果
// ↓
//-- pause --
//状態(before) : 再生中
//一時停止 : OK
//状態(after)  : 一時停止中

System.out.println("-- resume --");
System.out.println("状態(before) : " + player.getState());

player.resume();
System.out.println("状態(after)  : " + player.getState());

// 結果
// ↓
//-- resume --
//状態(before) : 一時停止中
//再開 : OK
//状態(after)  : 再生中

System.out.println("-- stop --");
System.out.println("状態(before) : " + player.getState());

player.stop();
System.out.println("状態(after)  : " + player.getState());

// 結果
// ↓
//-- stop --
//状態(before) : 再生中
//再生停止 : OK
//状態(after)  : 停止中

次に、状態遷移に失敗するケースです。
失敗した場合は、警告メッセージを出力します。

final var player = new VideoPlayer();

System.out.println("-- stop --");
System.out.println("状態 : " + player.getState());
player.stop();

// 結果
// ↓
//-- stop --
//状態 : 停止中
//警告 : すでに停止中です。

System.out.println("-- start --");
player.start();

// 結果
// ↓
//-- start --
//再生開始 : OK

System.out.println("-- start --");
System.out.println("状態 : " + player.getState());
player.start();

// 結果
// ↓
//-- start --
//状態 : 再生中
//警告 : すでに再生中です。

結果も問題なしですね。

State パターンを使わないケース に比べて、switch や if などの条件式も減りました … というより1つも使っていません。

もし新規の状態が追加されても、影響範囲の小さな修正で済みそうです。
VideoPlayer クラスについては修正なしで OK です。

これは、開放閉鎖の原則

  • ソフトウェア要素(クラス、モジュール、関数など)は、拡張に対しては開いており、修正に対しては閉じているべきである。

になっていますね。

まとめ

State パターンとは、

  • オブジェクトの状態を「振る舞いを持たせたクラスオブジェクト」で表す
  • それにより if や switch などの条件式を減らす

という設計です。

Strategy パターン の応用ともいえるので、よろしければそちらの記事も参考にしていただけたら幸いです。


関連記事

ページの先頭へ