Stateパターン


 ★☆☆

Stateパターン

状態の変化に応じて振る舞いを変える

State とは、英語で「状態」を意味する単語です。オブジェクト指向設計では、モノをクラスとして表現することが多くあります。しかし、State パターンとは、 モノではなく、「状態」をクラスとして表現するパターンです。

状態によって、動作のパターンが変わることがよくあります。


例えば、「機嫌のいい状態」「機嫌が悪い状態」の2つの状態があるお母さんにいくつか頼みごとをすることを考えます。機嫌のいい状態のお母さんに「お小遣い頂戴」「ケーキ買って」などのお願いをした場合、「はいはい」といってお小遣いをくれたり、ケーキを買ってくれたりするでしょう。

しかし、機嫌の悪い状態のお母さんにこれらのお願いをしても聞き入れてくれないかもしれません。 お母さんは状態によって、振る舞いが変わるわけです。


State パターンとは、このような、状態の変化に応じて振る舞いが変わるような場合に威力を発揮するパターンです。

Stateパターンを適用する目的は、「オブジェクトの内部状態が変化したときに、オブジェクトが振る舞いを変えるようにする。クラス内では、振る舞いの変化を記述せず、状態を表すオブジェクトを導入することでこれを実現する」ことです。「クラス内で振る舞いの変化を記述する」ことは問題だと考えられているわけです。パターンを適用するには、このようなことが問題であることに気づく技術が、開発者には必要です。問題であると気づかなければ、パターンを適用することを思いつくこともないでしょう。パターンを適用しなくてもプログラムは動作するからです。したがって、プログラムを変更するという作業を始めるには、「状態の数が増えたとき、これ以上プログラムの複雑度を上げてはいけない」という意識が必要です。


例題

時刻によって異なる挨拶を表示するクラスを作りなさい。


   4時~10時(朝) おはよう、さようなら

  10時~19時(昼) こんにちは、さようなら

  19時~ 4時(夜) こんばんは、おやすみ


実行結果
7時           
おはよう
さようなら
14時
こんにちは
さようなら
21時
こんばんは
おやすみ













Stateパターンを使用しない例

Greeting.java
public class Greeting { private int state; public void setClock(int hour) { System.out.println(hour + "時"); if (hour < 4 || hour >= 19) { state = 3; } else if (hour < 10) { state = 1; } else { state = 2; } System.out.println(encounter()); System.out.println(parting()); } public String encounter() { if (state == 1) { return "おはよう"; } else if (state == 2) { return "こんにちは"; } else { return "こんばんは"; } } public String parting() { if (state == 1) { return "さようなら"; } else if (state == 2) { return "さようなら"; } else { return "おやすみ"; } } }
Main.java
public class Main { public static void main(String[] args) { Greeting g = new Greeting(); g.setClock(7); g.setClock(14); g.setClock(21); } }

Stateパターンを使用した例

State.java
public abstract class State { public void clock(IContext context, int hour) { if (hour >= 4 && hour < 10) context.changeState(MorningState.getInstance()); else if (hour >= 10 && hour < 19) context.changeState(DayState.getInstance()); else if (hour >= 20 || hour < 4) context.changeState(NightState.getInstance()); } public abstract void encounter(IContext context); public abstract void parting(IContext context); }
MorningState.java
public class MorningState extends State { private static MorningState singleton = new MorningState(); public static State getInstance() { return singleton; } public void encounter(IContext context) { context.encounter("おはよう"); } public void parting(IContext context) { context.parting("さようなら"); } }
DayState.java
public class DayState extends State { private static DayState singleton = new DayState(); public static State getInstance() { return singleton; } public void encounter(IContext context) { context.encounter("こんにちは"); } public void parting(IContext context) { context.parting("さようなら"); } }
NightState.java
public class NightState extends State { private static NightState singleton = new NightState(); public static State getInstance() { return singleton; } public void encounter(IContext context) { context.encounter("こんばんは"); } public void parting(IContext context) { context.parting("おやすみ"); } }
IContext.java
public interface IContext { public void setClock(int hour); public void changeState(State state); public void encounter(String msg); public void parting(String msg); }
Greeting.java
public class Greeting implements IContext { private State state = NightState.getInstance(); public void setClock(int hour) { System.out.println(hour + "時"); state.clock(this, hour); state.encounter(this); state.parting(this); } public void changeState(State state) { this.state = state; } public void encounter(String msg) { System.out.println(msg); } public void parting(String msg) { System.out.println(msg); } }
Main.java
public class Main { public static void main(String[] args) { Greeting g = new Greeting(); g.setClock(7); g.setClock(14); g.setClock(21); } }

この例では、時刻によって state の状態を変え、挨拶を返すメソッドでその状態を if 文によって判定し、適当な文字列を返しています。出会ったときの挨拶を返すメソッド(encounter)にも if 文で分岐させて、分かれるときの挨拶を返すメソッド(parting)にも if 文で分岐させて、というように作ってあります。if 文で分岐させている箇所がいくつもありますが、一応動くので満足しています。

しかし、ここで、例えば(晩)という状態が増えたらどうでしょう。全部のメソッドの if 文を見直さなくてはならないことに、この段階になって気づくわけです。

このように、状態を追加する際に、全ての if 文の分岐を見直さなければならないような設計はあまり好ましいものではありません。

Stateパターンでは、機能でメソッドを分けるのではなく、状態で分けています。

この場合でも、もちろん時刻は見ていますが、State クラスだけです。状態が増えたり、状態の条件が変わったりすれば、変更しなければならないことは変わりませんが、状態を表すクラスだけですから変更のし忘れはないでしょう。

State パターンでは、「状態」を表すクラス、例であれば「朝の状態(MorningState)」「昼の状態(DayState)」 「夜の状態(NightState)」を用意し、この「状態」を入れ替え可能にしておきます。

そして、例えば、7時なら MorningState の インスタンスが、changeState というメソッドに引き渡され、そこで、
 state.encounter(this);
 state.parting(this);
というように、条件にあったインスタンスのメソッドが呼ばれることになります。

(state に MorningState の インスタンスがセットされている)


この例では、State クラスにclock メソッドが存在します。

ということは、State クラスは、サブクラスのことを知っている必要があるということです。 これは、将来 NightState クラスを削除することになったら、 State クラスも修正する必要があるということです。つまり、Stateクラスに状態遷移をまかせてしまうと、クラス間の依存関係を深めてしまうことになります。

別の方法として、この部分をChain of Responsibilityパターン(State の各サブクラスで、自分の時間帯ならば処理するし、そうでなければ次の時間帯に処理を委譲する)で記述すれば、変更しなければならない部分は減るでしょう。

また、状態遷移をGreeting クラスに任せてしまうこともできます。そうすれば、個々のStateのサブクラスの独立性が高まります。しかし、それではGreeting クラスがすべてのStateのサブクラスを知らなければならなくなります。この場合には、Mediatorパターンが適用できるかもしれません。