Java : State パターン (図解/デザインパターン)
State パターンとは、GoF によって定義されたデザインパターンの1つです。
オブジェクトの状態を「振る舞いを持たせたクラスオブジェクト」で表します。
本記事では、State パターンを Java のコード付きで解説していきます。
デザインパターン(GoF) 関連記事
- 生成に関するパターン
- 振る舞いに関するパターン
概要
State パターンとは、
- オブジェクトの状態を「振る舞いを持たせたクラスオブジェクト」で表す
- それにより if や switch などの条件式を減らす
という設計です。
State は、日本語的に発音すると「ステート」となります。
意味は「状態」ですね。
【State パターンのイメージ】
状態を持った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 などの条件式がコードから減らせます
- 条件式が減るとコードもシンプルになり保守も楽になります
- ユニットテストも作りやすくなるでしょう
関連記事:
クラス図とシーケンス図
上の図は、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 クラスを考えてみます。
VideoPlayer クラスには次のメソッドを用意します。
- start : 動作の再生を開始
- stop : 動画の再生を停止
- pause : 動画の再生を一時停止
- resume : 一時停止していた動画を再開
- getState : 現在の状態を文字列で取得
そして、この VideoPlayer クラスは、次の状態遷移をとります。
State パターンを使わないケース
まずは、State パターンを使わないでコードを書いてみましょう。
状態は 列挙値(enum) で表します。
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 パターンを使ってコードを改善してみましょう。
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 パターン の応用ともいえるので、よろしければそちらの記事も参考にしていただけたら幸いです。
関連記事
- 標準APIにならう命名規則
- コメントが少なくて済むコードを目指そう
- シングルトン・パターンの乱用はやめよう
- メソッドのパラメータ(引数)は使う側でチェックしよう
- 不変オブジェクト(イミュータブル) とは
- 依存性の注入(DI)をもっと気軽に
- 不要になったコードはコメントアウトで残さずに削除しよう
- 簡易的な Builder パターン
- 読み取り専用(const) のインタフェースを作る
- 図解/デザインパターン一覧 (GoF)
- Abstract Factory パターン
- Adapter パターン
- Bridge パターン
- Builder パターン
- Chain of Responsibility パターン
- Command パターン
- Composite パターン
- Decorator パターン
- Facade パターン
- Factory Method パターン
- Flyweight パターン
- Interpreter パターン
- Iterator パターン
- Mediator パターン
- Memento パターン
- Observer パターン
- Prototype パターン
- Proxy パターン
- Singleton パターン
- Strategy パターン
- Template Method パターン
- Visitor パターン