PHPによるデザインパターン入門 - State〜状態を表す
このエントリは、Do You PHP?(www.doyouphp.jp)で公開していたコンテンツを移行/加筆/修正したものです。公開の経緯はこちらをどうぞ。目次はこちらです。サンプルコードを手直ししたものをgithubに上げてありますのでそちらもどうぞ。
GoF本における分類
振る舞い+オブジェクト
はじめに
ここではStateパターンについて見ていきましょう。
stateという単語は「状態」の意味がありますが、Stateパターンは物ではなく「状態」をクラスとして表現し、「状態」ごとに振る舞いを切り替えられるようにするパターンです。
たとえば
たとえば、部屋の照明を考えてみましょう。照明には、点灯している状態(オン)と消灯している状態(オフ)の2つの状態があることになります。照明の状態がオンの場合、当然ですが照明が灯っている、つまり「明かりが灯る」という動作をしていると言えます。逆にオフの場合は「明かりが消える」という動作をしているということになります。このように「状態」によって振る舞いが変わるものはよくあります。
アプリケーションでも何らかの状態によって振る舞いが変わるものがあります。
たとえば、認証機能を持つアプリケーションの場合、認証済みの状態と認証していない状態が存在します。また、その状態によってメニュー表示を変えたり、特定の機能の動作を変えたりすることがあります。
このような場合、状態に依存する処理をどのように記述すれば良いでしょうか?おそらく、if文やswitch文を使って実装する場合が多いと思います。先の認証機能の例ですと状態は2種類しかありませんのでさほど問題にはならないかもしれません。しかし、状態の種類が多くなるとコードの可読性が落ち、保守性・拡張性を落とす原因になってしまいます。
また、状態に関するコードがあちこちに散らばってしまうことになります。このため、新しい状態を追加することが困難になります。
Stateパターンは、「状態」と「状態による振る舞い」を1つのクラスにまとめることで、先のような問題を解決します。
Stateパターンとは?
Stateパターンはオブジェクトの振る舞いに注目したパターンで、その名の通り「状態」をクラスとして表すことを目的としています。
GoF本では、Stateパターンの目的は次のように定義されています。
オブジェクトの内部状態が変化したときに、オブジェクトが振る舞いを変えるようにする。クラス内では振る舞いの変化を記述せず、状態を表すオブジェクトを導入することでこれを実現する。
Stateパターンでは、それぞれの具体的な状態をクラスとして定義します。これらの状態クラスは「どの状態か」を意識することなくアクセスできるよう共通のAPIを持っています。また、クライアントからアクセスさせるための処理クラスを用意し、その内部に状態クラスのインスタンスを保持します。この処理クラスは、クライアントからの要求を受け取った後、内部に保持した状態オブジェクトに実際の処理を任せています。
こうすることで、状態による振る舞いの変化を実現しています。
Stateパターンの構造
Stateパターンのクラス図と構成要素は、次のとおりです。
- Stateクラス
それぞれの状態に共通のAPIを定義します。このAPIは、状態固有の振る舞いをおこなうメソッドになります。Contextクラスからは、Stateクラスで定義されたAPIを通じて、ConcreteStateクラスで実装された具体的な処理を呼び出します。
- ConcreteStateクラス
Stateクラスのサブクラスで、Stateクラスで定義されたAPIを実装したクラスです。このクラスに、状態ごとの具体的な処理内容を記述します。
- Contextクラス
State型のオブジェクトを内部に保持し、具体的な処理をそのオブジェクトに委譲します。これにより、ConcreteStateクラスに依存することがなくなり、ConcreteStateクラスを簡単に切り替えることができます。
Stateパターンのメリット
Stateパターンのメリットとしては、以下のものが挙げられます。
- 状態に固有の処理をまとめることができる
Stateパターンを適用すると、それぞれの状態に固有な処理がクラスにまとめて実装されますので、コードを記述する際、その状態に固有な処理に専念できます。これにより、保守性が高まります。
また、新しい状態が追加された場合も、新しい状態クラスを作成するだけで済みます。
- 状態に固有の処理を選択するための条件文がなくなる
状態によって振る舞いを変える場合、1つのクラスやメソッドに処理を記述したくなってしまいます。しかし、クラスやメソッドごとにif文やswitch文を使って処理を分岐することになってしまい、コードの可読性を落としてしまいます。また、保守性・拡張性が下がることにもつながります。Stateパターンを適用すると、状態ごとの処理がクラス単位にまとめて実装されますので、if文やswitch文を使うことがなくなり、非常にすっきりしたコードになります。
Stateパターンの適用例
Stateパターンの適用例を見てみましょう。
ここでは簡単な認証機能にStateパターンを適用した例に取り上げます。
このアプリケーションには、認証機能のほか、簡単なカウンタ機能があります。このカウンタ機能はログインした状態の場合のみ利用できます。また、状態としては認証済み(ログイン)、未認証(ログアウト)の2つがあります。
では、早速コードを見ていきましょう。
まずは、Contextクラスに相当するUserクラスです。クライアントはこのUserクラスを利用しますが、その動作は内部に保持している状態オブジェクトによって変化します。
Userクラス(User.class.php)
<?php require_once 'UnauthorizedState.class.php'; /** * Contextクラスに相当する */ class User { private $name; private $state; private $count = 0; public function __construct($name) { $this->name = $name; // 初期値 $this->state = UnauthorizedState::getInstance(); $this->resetCount(); } /** * 状態を切り替える */ public function switchState() { echo "状態遷移:" . get_class($this->state) . "→"; $this->state = $this->state->nextState(); echo get_class($this->state) . "<br>"; $this->resetCount(); } public function isAuthenticated() { return $this->state->isAuthenticated(); } public function getMenu() { return $this->state->getMenu(); } public function getUserName() { return $this->name; } public function getCount() { return $this->count; } public function incrementCount() { $this->count++; } public function resetCount() { $this->count = 0; } }
また、状態を切り替えるためのメソッドswitchStateが定義されています。このメソッドの中に状態を切り替えるためのコードが記述されていますが、
$this->state = $this->state->nextState();
といった具合に内部に保持している状態オブジェクトのnextStateメソッドを呼び出しているだけです。状態オブジェクトには2種類あると説明しましたが、どの状態かを意識することなくnextStateメソッドを呼び出しています。この詳細の説明は、状態クラスの説明のときにおこないますので、ちょっと待っていてください。
次に状態を表すクラスたちを見ていきましょう。
まずは状態クラスに共通のAPIを定義しているUserStateインターフェースです。Stateクラスに相当します。
それぞれの具体的な状態クラスは、このインターフェースを実装することになります。
UserStateインターフェース(UserState.class.php)
<?php /** * Stateクラスに相当する * 状態毎の動作・振る舞いを定義する */ interface UserState { public function isAuthenticated(); public function nextState(); public function getMenu(); }
このインターフェースには3つのメソッドが定義されています。認証されているかどうかを返すisAuthenticatedメソッド、状態を切り替えるnextStateメソッド、その状態で利用可能なメニュー一覧を返すgetMenuメソッドです。
次は具体的な状態を表すクラスです。ConcreteStateクラスに相当します。
認証済みの状態を表すAuthorizedStateクラスと未認証の状態を表すUnauthorizedStateクラスの2クラスです。
AuthorizedStateクラス(AuthorizedState.class.php)
<?php require_once 'UserState.class.php'; require_once 'UnauthorizedState.class.php'; /** * ConcreteStateクラスに相当する * 認証後の状態を表すクラス */ class AuthorizedState implements UserState { private static $singleton = null; private function __construct() { } public static function getInstance() { if (self::$singleton == null) { self::$singleton = new AuthorizedState(); } return self::$singleton; } public function isAuthenticated() { return true; } public function nextState() { // 次の状態(未認証)を返す return UnauthorizedState::getInstance(); } public function getMenu() { $menu = '<a href="?mode=inc">カウントアップ</a> | ' . '<a href="?mode=reset">リセット</a> | ' . '<a href="?mode=state">ログアウト</a>'; return $menu; } /** * このインスタンスの複製を許可しないようにする * @throws RuntimeException */ public final function __clone() { throw new RuntimeException ('Clone is not allowed against ' . get_class($this)); } }
UnauthorizedStateクラス(UnauthorizedState.class.php)
<?php require_once 'UserState.class.php'; require_once 'AuthorizedState.class.php'; /** * ConcreteStateクラスに相当する * 未認証の状態を表すクラス */ class UnauthorizedState implements UserState { private static $singleton = null; private function __construct() { } public static function getInstance() { if (self::$singleton === null) { self::$singleton = new UnauthorizedState(); } return self::$singleton; } public function isAuthenticated() { return false; } public function nextState() { // 次の状態(認証)を返す return AuthorizedState::getInstance(); } public function getMenu() { $menu = '<a href="?mode=state">ログイン</a>'; return $menu; } /** * このインスタンスの複製を許可しないようにする * @throws RuntimeException */ public final function __clone() { throw new RuntimeException ('Clone is not allowed against ' . get_class($this)); } }
これらのクラスには、Singletonパターンも併せて適用されています。
注目していただきたいメソッドはnextStateメソッドです。このメソッドは次の状態に切り替えるためのメソッドですが、UnauthorizedStateクラスではAuthorizedStateクラスのインスタンス、AuthorizedStateクラスではUnauthorizedStateクラスのインスタンスを返すようになっています。つまり、「未認証」の次の状態は「認証済み」、「認証済み」の次の状態は「未認証」という状態の遷移を表しています。
Userクラスの内部では、このnextStateメソッドを使って状態の切り替えをおこなっていましたが、その際「現在どの状態なのか」を意識しないでnextStateメソッドを呼び出していたと思います。これは、状態クラスであるAuthorizedStateクラスやUnauthorizedStateクラス自身が次に遷移すべき状態を知っているからこそ可能になっています。
なお、新しい状態を増やす場合、AuthorizedStateクラスやUnauthorizedStateクラスと同様にUserStateインターフェースを実装したクラスを用意するだけで済みます。
最後にクライアントのコードです。このコードには、状態に関連するコードが一切出てきていないことを確認してください。
クライアント側コード(state_client.php)
<?php require_once 'User.class.php'; session_start(); $context = isset($_SESSION['context']) ? $_SESSION['context'] : null; if (is_null($context)) { $context = new User('ほげ'); } $mode = (isset($_GET['mode']) ? $_GET['mode'] : ''); switch ($mode) { case 'state': echo '<p style="color: #aa0000">状態を遷移します</p>'; $context->switchState(); break; case 'inc': echo '<p style="color: #008800">カウントアップします</p>'; $context->incrementCount(); break; case 'reset': echo '<p style="color: #008800">カウントをリセットします</p>'; $context->resetCount(); break; } $_SESSION['context'] = $context; echo 'ようこそ、' . $context->getUserName() . 'さん<br>'; echo '現在、ログインして' . ($context->isAuthenticated() ? 'います' : 'いません') . '<br>'; echo '現在のカウント:' . $context->getCount() . '<br>'; echo $context->getMenu() . '<br>';
このサンプルアプリケーションのクラス図は、次のようになります。
適用例の実行結果
Stateパターンを適用したサンプルの実行結果です。まずは、初期画面です。
「ログイン」リンクをクリックするとすると次のようになります。
この後、「カウントアップ」リンクを3回クリックすると次のようになります。
最後に、「ログアウト」リンクをクリックすると次のようになります。
Stateパターンのオブジェクト指向的要素
Stateパターンは「ポリモーフィズム」を活用しているパターンです。
Contextクラスは、State型のインスタンスを内部に保持します。このインスタンスは、具体的にはStateクラスを継承もしくは実装したConcreteStateクラスのインスタンスです。一方、Contextクラスは、クライアントから受け取った処理要求を、保持しているStateインスタンスに処理を委譲します。Contextクラス自身は状態に関する処理を一切行いません。この結果、保持したインスタンスが具体的にどのConcreteStateクラスのインスタンスなのかを意識することなく、振る舞いを変更することができるのです。併せて、ConcreteStateクラスを簡単に差し替えたり、追加したりできるのです。Stateパターンは、委譲を使って状態固有の処理を切り替えるパターンと言えます。
また、StateパターンとStrategyパターンは非常によく似たパターンですが、用いる場面は全く異なります。Stateパターンは状態によって振る舞いを変えますが、Strategyパターンは処理する対象の違いによって振る舞いを変えます。
関連するパターン
- Singletonパターン
Singletonパターンは、ConcreteStateクラスに適用できることが多いパターンです。
- Flyweightパターン
Singletonパターンと同様、ConcreteStateクラスにFlyweightパターンを適用できる場合があります。
まとめ
ここでは「状態」と「状態による振る舞い」を1つのクラスにまとめるStateパターンについて見てきました。