C言語からの本当のJava言語へ (State)
ちょっと間があきましたが、デザインパターンの続編。
今回はStateパターンです。
このパターンは名前からして「状態」に着目したパターンです。
も少し言うと、「その状態が行うべき動作」と「状態遷移」を上手いことまとめたパターンという感じです。
それでは例を交えて紹介します。
Stateパターン
簡単なところで電気を例にとってみましょう。
電気ってスイッチひとつで光ったり消えたりしますね。ではプログラムにするとどんな風になると思いますか?
スイッチメソッドが呼ばれ、ステータスが消えた状態なら電気をつける!ステータスが光ってる状態なら電気を消す!ですよね。
確かに問題ないですし、プログラムもちゃんと動きそうです。
しかしもちっとクールに書けるんです。それが今回紹介するStateパターン。
状態というものをクラスにしてしまいます。
光ってるクラスと消えてるクラス。
それぞれに必要な処理を入れるのですが、ここでミソなのが状態遷移もそれぞれのクラスで管理しちゃおうってとこです。
状態遷移図としてはこのようになりますね。
光った状態か消えた状態か。。
ってことは、それぞれのクラスは自身の状態の次を知ってるということです。
光ってるクラスなら、「俺の次の状態は消えた状態になるんや!」って知ってるってことですね。
消えてるクラスも同様です。
さらに言うと、スイッチを押す人は次どんな状態の動作をすべきか知らなくて良いことになります。
これはとても素敵なことです。ある意味ここが重要なところです。
実際にコードを見てみましょう。
状態インターフェース
public interface State { public State nextState(); public void display(); }
光ってるクラス
public class OnState implements State { private static State instance = new OnState(); private OnState() {} public static State getInstance() { return instance; } @Override public State nextState() { // 次の状態:消えた状態 return OffState.getInstance(); } @Override public void display() { System.out.println("電気光ってますよ!☆"); } }
消えてるクラス
public class OffState implements State { private static State instance = new OffState(); private OffState() {} public static State getInstance() { return instance; } @Override public State nextState() { // 次の状態:光った状態 return OnState.getInstance(); } @Override public void display() { System.out.println("電気消えてますよ!★"); } }
メインクラス
public class Main { public static void main(String args[]) throws Exception { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); // 初期状態設定:消えてる状態にする State state = OffState.getInstance(); state.display(); while (true) { System.out.print("Enterを押してスイッチを切り替えて下さい"); br.readLine(); // スイッチを切替えて状態遷移 state = state.nextState(); state.display(); } } }
そしてこのプログラムを実行した状態がこうなります。
電気消えてますよ!★ Enterを押してスイッチを切り替えて下さい 電気光ってますよ!☆ Enterを押してスイッチを切り替えて下さい 電気消えてますよ!★ Enterを押してスイッチを切り替えて下さい 電気光ってますよ!☆ Enterを押してスイッチを切り替えて下さい
Enterキー(コード上何のキー操作でもよいのですが・・)を押下する度に電気が光ったり消えたりしてますよね。
先ほど言った通りスイッチを押す人(Mainクラス)は次の状態を意識してないです。
ただ、スイッチを押すということだけしかやらなくてよいという単純な動きになります。
上記のソースコードをクラス図にするとこうなります。
<<クラス図>>
短いコードなので有り難みをあまり感じられないかもしてませんが、機能追加して見ましょう。
スイッチ押すたびに、光る→点滅→消える→光る→点滅…のように間に点滅状態が増えるとどうでしょう。
スイッチ押すクラス(Mainクラス)、消えてるクラス(OffStateクラス)は触りません。
光ってるクラスの次の状態遷移先が点滅クラスになり、新たに点滅クラスを追加するだけです。
光ってるクラス修正
public State nextState() { // 次の状態:消えた状態 //return OffState.getInstance(); //遷移先を変える // 次の状態:点滅した状態 return FlashState.getInstance(); }
点滅してるクラス追加
public class FlashState implements State { private static State instance = new FlashState(); private FlashState() {} public static State getInstance() { return instance; } @Override public State nextState() { // 次の状態:消えた状態 return OffState.getInstance(); } @Override public void display() { System.out.println("電気点滅してますよ!★☆"); } }
実行結果
電気消えてますよ!★ Enterを押してスイッチを切り替えて下さい 電気光ってますよ!☆ Enterを押してスイッチを切り替えて下さい 電気点滅してますよ!★☆ Enterを押してスイッチを切り替えて下さい 電気消えてますよ!★ Enterを押してスイッチを切り替えて下さい 電気光ってますよ!☆ Enterを押してスイッチを切り替えて下さい 電気点滅してますよ!★☆ Enterを押してスイッチを切り替えて下さい 電気消えてますよ!★ Enterを押してスイッチを切り替えて下さい
どうですか。
機能拡張がとても簡単に感じられたかと思います。
手続き型の書き方ですと、if文で各種状態が分岐されて、その時の動作を書き並べた感じになりますね。
そうなると…テストをするのが…っと思うでしょう。
他のパターンもそうですが、オブジェクト指向で設計されたプログラムは、可読性と拡張性が上がる、
また単体テストが楽になるというところが良いところですね。
なので、状態によって動作する場合や、状態遷移がある場合など、このStateパターンを当てはめてみると
いい感じに設計が進むことと思います。
・ログインしている時に表示するメニュー
・ログアウトした時に表示するメニュー
こんな時にも使えるのではないでしょうか。