PHP7でデザインパターン入門19/23 Stateパターン

State パターンとは

振る舞いに関する(英語版) デザインパターンの一種である。このパターンはオブジェクトの状態(state)を表現するために用いられる。

State パターン - Wikipedia

Stateパターンでは、「状態」というものをクラスとして表現する。「状態」をクラスで表現していれば、クラスを切り替えることにより「状態の変化」を表すことができ、新しい状態を追加しなければならないときに何をプログラムすればよいかが明確になる。

example

時刻ごとに警備状態が変化する金庫警備システムを考える。 1時間ごとに擬似時刻を変化させ、その都度ランダムにアクションを実行させる。

仕様

  • 金庫が1つある
  • 金庫は警備センターと接続されている
  • 金庫には非常ベルト通常通話のようの電話が接続されている
  • 金庫には時計がついていて、現在時刻を監視している。
  • 昼間は9:00-16:59, 夜間は17:00-23:59および0:00-8:59の範囲
  • 金庫は昼間だけ利用可能
  • 昼間、金庫を利用すると、警備センターに使用記録が残る
  • 夜間、金庫を利用すると、警備センターに非常事態の通報が送信される
  • 非常ベルはいつでも使用可能
  • 非常ベルを使用すると、警備センターに非常事態の通報が送信される
  • 通常通話用の電話はいつでも使用可能。(が、夜間は録音のみ)
  • 昼間、電話を使用すると、警備センターが呼び出される
  • 夜間、電話を使用すると、警備センターの留守録が呼び出される

State インターフェース

金庫の状態を表す。以下のアクションに対応して呼び出されるインターフェースを定義している。

  • 時刻が設定されたとき
  • 金庫が使用されたとき
  • 非常ベルが押されたとき
  • 通常通話を行うとき
<?php
interface State
{
    public function doClock(Context $context, int $hour);
    public function doUse(Context $context);
    public function doAlarm(Context $context);
    public function doPhone(Context $context);
}

DayStateクラス

昼間の状態を表すクラス。状態を表すクラスは1つずつしかインスタンスを作らないことにする。 状態が変化するたびに新しいインスタンスを作るのはメモリと処理時間の無駄なのでSingletonパターンを使っている。

<?php

class DayState implements State
{
    private static $singleton;

    private function __construct()
    {
    }

    public static function getInstance(): State
    {
        if (self::$singleton === null) {
            self::$singleton = new self;
        }
        return self::$singleton;
    }

    public function doClock(Context $context, int $hour)
    {
        if ($hour < 9 || 17 <= $hour) {
            $context->changeState(NightState::getInstance());
        }
    }

    public function doUse(Context $context)
    {
        $context->recordLog("use the strongbox(daytime)");
    }

    public function doAlarm(Context $context)
    {
        $context->callSecurityCenter("security alert(daytime)");
    }

    public function doPhone(Context $context)
    {
        $context->callSecurityCenter("call security center(daytime)");
    }

    public function __toString(): string
    {
        return "[daytime]";
    }
}

NightStateクラス

夜間の状態を表すクラス。構成はDayStateクラスと同じ。

<?php

class NightState implements State
{
    private static $singleton;

    private function __construct()
    {
    }

    public static function getInstance(): State
    {
        if (self::$singleton === null) {
            self::$singleton = new self;
        }
        return self::$singleton;
    }

    public function doClock(Context $context, int $hour)
    {
        if (9 <= $hour && $hour < 17) {
            $context->changeState(DayState::getInstance());
        }
    }

    public function doUse(Context $context)
    {
        $context->callSecurityCenter("emergency:use the strongbox at nighttime");
    }

    public function doAlarm(Context $context)
    {
        $context->callSecurityCenter("security alert(nighttime)");
    }

    public function doPhone(Context $context)
    {
        $context->recordLog("record of the night call");
    }

    public function __toString(): string
    {
        return "[nighttime]";
    }
}

Contextクラス

状態の管理、警備センターへの呼び出しを行う。

<?php

class Context
{
    private $state;

    public function __construct(State $state = null)
    {
        if ($state instanceof State) {
            $this->state = $state;
        } else {
            $this->state = DayState::getInstance();
        }
    }

    public function setClock(int $hour)
    {
        $clockstring = "now: ";
        if ($hour < 10) {
            $clockstring .= "0" . $hour . ":00";
        } else {
            $clockstring .= $hour . ":00";
        }
        echo $clockstring . PHP_EOL;
        $this->state->doClock($this, $hour);
    }

    public function changeState(State $state)
    {
        echo "state is change from " . $this->state . " to " . $state . PHP_EOL;
        $this->state = $state;
    }

    public function callSecurityCenter(string $msg)
    {
        echo "call! " . $msg . PHP_EOL;
    }

    public function recordLog(string $msg)
    {
        echo "record ... " . $msg . PHP_EOL;
    }

    public function doRandomAction()
    {
        switch (rand(0, 2)) {
            case 0:
                $this->state->doUse($this);
                break;
            case 1:
                $this->state->doAlarm($this);
                break;
            case 2:
                $this->state->doPhone($this);
                break;
            default:
                break;
        }
    }
}

Main

テスト動作用

<?php

$context = new Context();
for ($hour = 0; $hour < 24; $hour++) {
    $context->setClock($hour);
    $context->doRandomAction();
    sleep(1);
}

実行結果

$ php Main.php
now: 00:00
state is change from [daytime] to [nighttime]
call! security alert(nighttime)
now: 01:00
call! emergency:use the strongbox at nighttime
now: 02:00
call! security alert(nighttime)
now: 03:00
call! security alert(nighttime)
now: 04:00
call! emergency:use the strongbox at nighttime
now: 05:00
call! emergency:use the strongbox at nighttime
now: 06:00
call! emergency:use the strongbox at nighttime
now: 07:00
call! emergency:use the strongbox at nighttime
now: 08:00
record ... record of the night call
now: 09:00
state is change from [nighttime] to [daytime]
call! call security center(daytime)
... 中略 ....
now: 18:00
call! emergency:use the strongbox at nighttime
now: 19:00
call! security alert(nighttime)
now: 20:00
record ... record of the night call
now: 21:00
call! security alert(nighttime)
now: 22:00
call! emergency:use the strongbox at nighttime
now: 23:00
record ... record of the night call

ソースコード

github.com

役割

State役

状態を表す。状態毎に異なる振る舞いをするインターフェースを定義する。サンプルコードにおけるStateインターフェース。

ConcreteState役

具体的な個々の状態を表現する。State役で定められたインターフェースを具体的に実装する。 サンプルコードにおけるDayState, NightStateクラス。

Context役

現在の状態を表すConcreteState役を持つ。 サンプルコードにおけるContextクラス。

まとめ

分割統治

Stateパターンは、システムの状態をクラスとして表現することで、複雑なプログラムを分割する

状態に依存した処理

Stateインターフェースで宣言されているメソッドはすべて「状態に依存した処理」である。「状態に依存した処理」は、プログラム上で以下のように表現される。

  • 抽象メソッドとして宣言し、インターフェースとする。
  • 具象メソッドとして実装し、個々のクラスとする。

自己矛盾が起こらない

Stateパターンでは状態をクラスで表現する。現在の状態を表す変数はたった1つである。サンプルコードではstateプロパティがシステムの状態を決定している。

新しい状態の追加は容易

サンプルコードで言えば、Stateインターフェースを実装したXXXStateクラスの追加は容易。逆にStateインターフェースに新しいメソッドを追加することは、全てのXXXStateクラスへの変更が必要となり、困難となる。

関連パターン

増補改訂版Java言語で学ぶデザインパターン入門

増補改訂版Java言語で学ぶデザインパターン入門