デザインパターン State (php&ruby)

デザインパターンのStateは、「状態」をクラスで管理します。
たとえば、ログインしている状態、ログインしていない状態、という2種類の「状態」があります。
それらの状態によって振る舞いが異なるとき、別々のクラスに書き分けることによって、利用側は状態の変更を通知するだけでよく、クラス側は状態によって条件分岐のロジックを仕込まずに済むようになります。
ここでは、実際にログインしている|していない、の状態をデザインパターンのStateを使って、phpとrubyでそれぞれ実装してみます。
まず、クラス図をみてみましょう。
State_pattern_2
Userクラスは利用側でインスタンスされる唯一のクラスです。このクラスは内部に「状態」オブジェクト(UserState)を保持していて、ポリモーフィズムを使った共通のインターフェース(isAuth(), getMenu(), doEdit(), doExit())を中継します。状態を切り替えるためのswitchState()メソッドが叩かれると、ラッピングされたnextStage()インターフェースを経由して、内部に保持した「状態」オブジェクトをまるごと入れ替えます。
UserStateクラスは、「状態」を表すクラスに共通のインターフェースを付与するためのインターフェースクラスです。
AuthStateクラスUnauthStateクラスは、それぞれログインしている|していない、を表すクラスです。
着目したいのは、nextStage()メソッドが、前者ではログインしていない状態オブジェクト(UnauthState)を、後者ではログインしている状態オブジェクト(AuthState)を返却していることです。これは、受け取る側のUserクラスからみると、「状態」がまるごと入れ替わることを意味します。
これによって、開発者は状態の違いによる固有な振る舞いを、それぞれのクラスに記述することだけに集中することができます。
いよいよコードをみてみましょう。
まずphpです。
User.class.php (Userクラス)

<?php
Class User
{
private $_name = null;
private $_state = null;
public function __construct($name) {
$this->_name = $name;
$this->_state = UnauthState::getInstance();
}
public function switchState() {
$this->_state = $this->_state->nextStage();
}
public function isAuth() {
return $this->_state->isAuth();
}
public function getMenu() {
return $this->_state->getMenu();
}
public function doEdit() {
return $this->_state->doEdit();
}
public function doExit() {
return $this->_state->doExit();
}
public function getName() {
return $this->_name;
}
}

UserState.class.php (UserStateクラス)

<?php
Interface UserState
{
public function isAuth();
public function nextStage();
public function getMenu();
public function doEdit();
public function doExit();
}

AuthState.class.php (AuthStateクラス)

<?php
Class AuthState implements UserState
{
private static $_singleton = null;
private function __construct() {
}
public static function getInstance() {
if (self::$_singleton == null) self::$_singleton = new AuthState();
return self::$_singleton;
}
public function isAuth() {
return true;
}
public function nextStage() {
return UnauthState::getInstance();
}
public function getMenu() {
echo "now login...\n  1: edit\n  2: logout\n  input number: ";
return intval(chop(fgets(STDIN)));
}
public function doEdit() {
echo "edit...";
return true;
}
public function doExit() {
return false;
}
public final function __clone() {
throw new RuntimeException('cannot make clone: ' . get_class($this));
}
}

UnauthState.class.php (UnauthStateクラス)

<?php
Class UnauthState implements UserState
{
private static $_singleton = null;
private function __construct() {
}
public static function getInstance() {
if (self::$_singleton == null) self::$_singleton = new UnauthState();
return self::$_singleton;
}
public function isAuth() {
return false;
}
public function nextStage() {
return AuthState::getInstance();
}
public function getMenu() {
echo "now logout...\n  2: login\n  3: exit\n  input number: ";
return intval(chop(fgets(STDIN)));
}
public function doEdit() {
return false;
}
public function doExit() {
echo "bye\n";
return true;
}
public final function __clone() {
throw new RuntimeException('cannot make clone: ' . get_class($this));
}
}

state_client.php (利用側)

<?php
require 'User.class.php';
require 'UserState.class.php';
require 'AuthState.class.php';
require 'UnauthState.class.php';
define('MODE_EDIT',  1);
define('MODE_STATE', 2);
define('MODE_EXIT',  3);
$context = new User('hoge');
$mode = null;
while (true) {
switch ($mode) {
case MODE_STATE :
$context->switchState();
if ($context->isAuth()) echo 'welcome ' . $context->getName() . "\n";
break;
case MODE_EDIT :
if ($context->doEdit()) echo "OK\n";
break;
case MODE_EXIT :
if ($context->doExit()) exit();
break;
}
$mode = $context->getMenu();
}

次にrubyです。
User.class.rb (Userクラス)

class User
def initialize(name)
@_name = name
@_state = UnauthState.instance
end
def switchState
@_state = @_state.nextStage
end
def isAuth
@_state.isAuth
end
def getMenu
@_state.getMenu
end
def doEdit
@_state.doEdit
end
def doExit
@_state.doExit
end
def getName
@_name
end
end

UserState.class.rb (UserStateクラス)

class NotImplements < Exception;end
module UserState
def isAuth
raise NotImplements
end
def nextStage
raise NotImplements
end
def getMenu
raise NotImplements
end
def doEdit
raise NotImplements
end
def doExit
raise NotImplements
end
end

AuthState.class.rb (AuthStateクラス)

class AuthState
include UserState
include Singleton
def isAuth
true
end
def nextStage
UnauthState.instance
end
def getMenu
print "now login...\n  1: edit\n  2: logout\n  input number: "
STDIN.gets.chomp.to_i
end
def doEdit
print "edit..."
true
end
def doExit
false
end
end

UnauthState.class.rb (UnauthStateクラス)

class UnauthState
include UserState
include Singleton
def isAuth
false
end
def nextStage
AuthState.instance
end
def getMenu
print "now logout...\n  2: login\n  3: exit\n  input number: "
STDIN.gets.chomp.to_i
end
def doEdit
false
end
def doExit
print "bye\n"
true
end
end

state_client.rb (利用側)

require 'singleton'
require 'User.class.rb'
require 'UserState.class.rb'
require 'AuthState.class.rb'
require 'UnauthState.class.rb'
MODE_EDIT  = 1
MODE_STATE = 2
MODE_EXIT  = 3
context = User.new('hoge')
mode = nil
while(true)
case mode
when MODE_STATE
context.switchState
print "welcome #{context.getName}\n" if context.isAuth
when MODE_EDIT
print "OK\n" if context.doEdit
when MODE_EXIT
exit if context.doExit
end
mode = context.getMenu
end

php ./state_client.php | ruby ./state_client.rb (php&ruby共通の実行結果)

now logout...
2: login
3: exit
input number: 2
welcome hoge
now login...
1: edit
2: logout
input number: 1
edit...OK
now login...
1: edit
2: logout
input number: 2
now logout...
2: login
3: exit
input number: 3
bye

まとめ:
サービスをモデル化してクラスに落とし込む際に、どうしても名詞抽出法に頭がとらわれると、なかなか概念モデルをクラスにあてはめるという機知が働かないのですが、こういったデザインパターンをおさえておくと、そのサービスにとって主要で明確な振る舞いを持つ概念をクラス化してみることで、ずいぶんとif文やswitch文が減らせて可読性があがり、如いては堅牢性や汎用性も高まるという恩恵を受けられるかもしれませんね。
デザインパーンでは他にも「オブジェクト」にとらわれないモデリングの仕方が多々あります。機会があれば別のパターンも紹介できればと思います。
phpとrubyを並べて書くと、やっぱりrubyの方が楽で早いわあ、と感じます。シングルトンなんてモジュールで提供されているので、phpみたいに自前でこしらえる必要がありません。アジャイル開発なら断然phpよりrubyですね。
なお、phpはversion5.3.1、rubyはversion1.9.1を使用しています。
動作結果はシェルからコマンドとして実行したものです。
参考書籍: PHPによるデザインパターン入門