デザインパターンのStateは、「状態」をクラスで管理します。
たとえば、ログインしている状態、ログインしていない状態、という2種類の「状態」があります。
それらの状態によって振る舞いが異なるとき、別々のクラスに書き分けることによって、利用側は状態の変更を通知するだけでよく、クラス側は状態によって条件分岐のロジックを仕込まずに済むようになります。
ここでは、実際にログインしている|していない、の状態をデザインパターンのStateを使って、phpとrubyでそれぞれ実装してみます。
まず、クラス図をみてみましょう。
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によるデザインパターン入門