本記事に掲載したサンプルコードは、https://github.com/ryo-utsunomiya/design_patternでも公開中です。
Stateとは
「状態」をクラスとして表現する方法です。複雑な条件文をリファクタリングする際などに使用されます。
ここでは、「昼」「夜」という状態を実装し、状態に応じて表示されるメッセージが変わるアプリケーションを実装します。
まずは、State
のインタフェース。
<?php
namespace Statemy;
interface StateInterface
{
/**
* @param ContextInterface $context
* @param int $hour
* @return void
*/
public function doClock(ContextInterface $context, $hour);
/**
* @return string
*/
public function __toString();
}
doClock
メソッドで使用しているContextInterface
は、状態に応じた振る舞い(この例では、メッセージ出力)を行うオブジェクトのインタフェースです。定義は以下のようになります。
<?php
namespace Statemy;
interface ContextInterface
{
/**
* @param int $hour
* @return void
*/
public function setClock($hour);
/**
* @param StateInterface $state
* @return void
*/
public function changeState(StateInterface $state);
}
次に、昼を表すDayState
。
<?php
namespace Statemy;
class DayState implements StateInterface
{
/**
* @var DayState
*/
private static $singleton;
private function __construct()
{
}
/**
* @return DayState
*/
public static function getInstance()
{
if (is_null(self::$singleton)) {
self::$singleton = new self();
}
return self::$singleton;
}
/**
* @param int $hour
* @return bool
*/
public static function isDay($hour)
{
return 9 <= $hour && $hour < 17;
}
/**
* @param ContextInterface $context
* @param int $hour
*/
public function doClock(ContextInterface $context, $hour)
{
if (!self::isDay($hour)) {
$context->changeState(NightState::getInstance());
}
}
/**
* @return string
*/
public function __toString()
{
return '[day]';
}
}
DayState
はSingletonで実装しています。これは、状態が変わる度に新しいインスタンスが生成されることを防ぐためです。また、isDay
というメソッドを持っていて、渡された時刻が昼か夜かを判別します。
昼か夜かを判別する役割をどのクラスに持たせるかは悩みましたが、DayStateに持たせました。今回はオリジナルに準拠してStateをインタフェースにしていますが、抽象クラスを作って、これに昼か夜かを判別するメソッドを持たせるのが良いかもしれません。
次に、夜を表すNightState
。
<?php
namespace Statemy;
class NightState implements StateInterface
{
/**
* @var NightState
*/
private static $singleton;
private function __construct()
{
}
/**
* @return NightState
*/
public static function getInstance()
{
if (is_null(self::$singleton)) {
self::$singleton = new self();
}
return self::$singleton;
}
/**
* @param ContextInterface $context
* @param int $hour
*/
public function doClock(ContextInterface $context, $hour)
{
if (DayState::isDay($hour)) {
$context->changeState(DayState::getInstance());
}
}
/**
* @return string
*/
public function __toString()
{
return '[night]';
}
}
次に、DayState・NightStateを使うSafe(金庫)クラスです(オリジナルではGUIを含んでいますが、PHPでGUIを作るのは大変なので割愛します)。
<?php
namespace Statemy;
class Safe implements ContextInterface
{
/**
* @var StateInterface
*/
private $state;
/**
* @param int $hour
*/
public function __construct($hour)
{
if (DayState::isDay($hour)) {
$this->state = DayState::getInstance();
} else {
$this->state = NightState::getInstance();
}
}
/**
* @param int $hour
* @return void
*/
public function setClock($hour)
{
$clockMessage = 'Now:';
if ($hour < 10) {
$clockMessage .= '0' . $hour . ':00';
} else {
$clockMessage .= $hour . ':00';
}
echo $clockMessage . $this->state . PHP_EOL;
$this->state->doClock($this, $hour);
}
/**
* @param StateInterface $state
* @return void
*/
public function changeState(StateInterface $state)
{
echo 'State is changed from ' . $this->state . ' to ' . $state . PHP_EOL;
$this->state = $state;
}
}
最後に、これらのクラスを使用するmain.phpです。
<?php
namespace Statemy;
require_once __DIR__ . '/../autoload.php';
$hour = 0;
$safe = new Safe($hour);
while (true) {
if ($hour === 24) {
$hour = 0;
} else {
$hour++;
}
$safe->setClock($hour);
sleep(1);
}
現在時刻の変化に応じて、Stateが自動的に切り替わります。状態が2つだけだと、ありがたみがわかりづらいですが、Stateパターンの強みは新しい状態の追加のような仕様変更に柔軟に対応できる、という点にあります。