Design pattern summary
This design pattern is classified in the same category than the observer / observable
It allow to update an object when it's state is updated
TopUsage
We will take a topic from a message board, like phpbb or phorums one. A topic can:
- Be empty
- Have messages
- Being locked
In each of thoses states, we could try for exemple to:
- Add or remove a related answer
- Lock or unlock the topic
Theses possibles actions will be implemented with an interface:
<?php
/**
* $Id$
* @package Poo_Example
* @subpackage State
*/
if (!interface_exists('Forum_Topic_State_Interface')) {
/**
*
*/
if (!defined('__EXAMPLE_PATH__')) {
define('__EXAMPLE_PATH__', realpath(dirname(__FILE__).'/../../../'));
}
require_once __EXAMPLE_PATH__ . '/Autoload.php';
/**
* our state interface
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
* @package Poo_Example
* @subpackage State
*/
interface Forum_Topic_State_Interface
{
/**
* constructor
* @var Forum_Topic $topic the topic analysed
* @access public
* @return void
*/
public function __construct(Forum_Topic $topic);
/**
* add a message to our topic
* @var Forum_Message $message : the message to add
* @access public
* @return boolean
* @throws Forum_Topic_Exception
*/
public function addAnswer(Forum_Message $message);
/**
* remove a message from our topic
* @var Forum_Message $message the message to delete
* @access public
* @return boolean
* @throws Forum_Topic_Exception
*/
public function deleteAnswer(Forum_Message $message);
/**
* unlock the topic
* @access public
* @throws Forum_Topic_Exception
*/
public function unlockTopic();
/**
* lock the topic
* @access public
* @throws Forum_Topic_Exception
*/
public function lockTopic();
}
}
All of our states need to have theses possibles actions
TopWithout this design pattern
Their is multiple ways to write this kind of subjects, google is your friend :)
TopWith design pattern
We define one class per state. All of our state use the Forum_Topic_State_Interface:
Le locked state
In this case, We can't delete or add answers, only unlock the topic
<?php
/**
* $Id$
* @package Poo_Example
* @subpackage State
*/
if (!class_exists('Forum_Topic_State_Locked')) {
/**
*
*/
if (!defined('__EXAMPLE_PATH__')) {
define('__EXAMPLE_PATH__', realpath(dirname(__FILE__).'/../../../'));
}
require_once __EXAMPLE_PATH__ . '/Autoload.php';
/**
* the state locked of our topic
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
* @package Poo_Example
* @subpackage State
*/
class Forum_Topic_State_Locked implements Forum_Topic_State_Interface
{
/**
* @var Forum_Topic $_topic the related topic
* @access private
*/
private $_topic = null;
/**
* constructor
* @var Forum_Topic $topic the topic analysed
* @access public
* @return void
*/
public function __construct(Forum_Topic $topic)
{
$this->_topic = $topic;
}
/**
* add a message to our topic
* @var Forum_Message $message : the message to add
* @access public
* @return boolean
* @throws Forum_Topic_Exception
*/
public function addAnswer(Forum_Message $message)
{
throw new Forum_Topic_Exception('topic is locked');
}
/**
* remove a message from our topic
* @var Forum_Message $message the message to delete
* @access public
* @return boolean
*/
public function deleteAnswer(Forum_Message $message)
{
throw new Forum_Topic_Exception('topic is locked');
}
/**
* unlock the topic
* @access public
*/
public function unlockTopic()
{
// according with related answers number, we define the topic state
$this->_topic->setState(
$this->_topic->haveAnswers() ?
new Forum_Topic_State_WithAnswers($this->_topic) :
new Forum_Topic_State_WithNoAnswers($this->_topic));
}
/**
* lock the topic
* @access public
* @throws Forum_Topic_Exception
*/
public function lockTopic()
{
throw new Forum_Topic_Exception('topic is already locked');
}
/**
* @access public
* @return string
*/
public function __toString()
{
return get_class($this);
}
}
}
State without related answers
In this case we can't remove related answers or unlock topic, but we could lock it or add an answer
<?php
/**
* $Id$
* @package Poo_Example
* @subpackage State
*/
if (!class_exists('Forum_Topic_State_WithNoAnswers')) {
/**
*
*/
if (!defined('__EXAMPLE_PATH__')) {
define('__EXAMPLE_PATH__', realpath(dirname(__FILE__).'/../../../'));
}
require_once __EXAMPLE_PATH__ . '/Autoload.php';
/**
* the state of our topic when it have no answers
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
* @package Poo_Example
* @subpackage State
*/
class Forum_Topic_State_WithNoAnswers implements Forum_Topic_State_Interface
{
/**
* @var Forum_Topic $_topic the related topic
* @access private
*/
private $_topic = null;
/**
* constructor
* @var Forum_Topic $topic the topic analysed
* @access public
* @return void
*/
public function __construct(Forum_Topic $topic)
{
$this->_topic = $topic;
}
/**
* add a message to our topic
* @var Forum_Message $message : the message to add
* @access public
* @return boolean
*/
public function addAnswer(Forum_Message $message)
{
$answers = & $this->_topic->getMessages();
$answers[] = $message;
// we are sure that we have related answers
$this->_topic->setState(new Forum_Topic_State_WithAnswers($this->_topic));
}
/**
* {method_addanswer}
* @var Forum_Message $message : the message to add
* @access public
* @return boolean
* @throws Forum_Topic_Exception
*/
public function deleteAnswer(Forum_Message $message)
{
throw new Forum_Topic_Exception('this topic have no related message');
}
/**
* unlock the topic
* @access public
* @throws Forum_Topic_Exception
*/
public function unlockTopic()
{
throw new Forum_Topic_Exception('topic is already unlocked');
}
/**
* lock the topic
* @access public
* @throws Forum_Topic_Exception
*/
public function lockTopic()
{
$this->_topic->setState(new Forum_Topic_State_Locked($this->_topic));
}
/**
* @access public
* @return string
*/
public function __toString()
{
return get_class($this);
}
}
}
State with related answers
In this case, the only action which does not have meaning is to unlock topic
<?php
/**
* $Id$
* @package Poo_Example
* @subpackage State
*/
if (!class_exists('Forum_Topic_State_WithAnswers')) {
/**
*
*/
if (!defined('__EXAMPLE_PATH__')) {
define('__EXAMPLE_PATH__', realpath(dirname(__FILE__).'/../../../'));
}
require_once __EXAMPLE_PATH__ . '/Autoload.php';
/**
* the state of our topic when it have yet answers
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
* @package Poo_Example
* @subpackage State
*/
class Forum_Topic_State_WithAnswers implements Forum_Topic_State_Interface
{
/**
* @var Forum_Topic $_topic the related topic
* @access private
*/
private $_topic = null;
/**
* constructor
* @var Forum_Topic $topic the topic analysed
* @access public
* @return void
*/
public function __construct(Forum_Topic $topic)
{
$this->_topic = $topic;
}
/**
* add a message to our topic
* @var Forum_Message $message : the message to add
* @access public
* @return void
* @throws Forum_Topic_Exception
*/
public function addAnswer(Forum_Message $message)
{
$answers = & $this->_topic->getMessages();
$answers[] = $message;
}
/**
* remove a message from our topic
* @var Forum_Message $message the message to delete
* @access public
* @return boolean
* @throws Forum_Topic_Exception
*/
public function deleteAnswer(Forum_Message $message)
{
$answers = & $this->_topic->getMessages();
if (($key = array_search($message, $answers)) === false)
{
throw new Forum_Topic_Exception('this topic have no related answers');
}
unset($answers[$key]);
// our topic could change its state here
if (!$this->_topic->haveAnswers()) {
$this->_topic->setState(new Forum_Topic_State_WithNoAnswers($this->_topic));
}
return true;
}
/**
* unlock the topic
* @access public
* @throws Forum_Topic_Exception
*/
public function unlockTopic()
{
throw new Forum_Topic_Exception('topic is already unlocked');
}
/**
* lock the topic
* @access public
* @throws Forum_Topic_Exception
*/
public function lockTopic()
{
$this->_topic->setState(new Forum_Topic_State_Locked($this->_topic));
}
/**
* @access public
* @return string
*/
public function __toString()
{
return get_class($this);
}
}
}
Now we have our states, here's a possible Topic class:
<?php
/**
* $Id$
* @package Poo_Example
* @subpackage State
*/
if (!class_exists('Forum_Topic')) {
/**
* our topic forum class
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
* @package Poo_Example
* @subpackage State
*/
class Forum_Topic {
/**
* @access private
* @var Forum_Topic_State_Interface $state our topic state
*/
private $_state = null;
/**
* @access private
* @var array $_messages : the related answers
*/
private $_messages = array();
/**
* constructor
* @access public
*/
public function __construct()
{
// we set the first state to a topic without answers
$this->_state = new Forum_Topic_State_WithNoAnswers($this);
}
/**
* add an answer to our topic
* @access public
* @return void
* @param Forum_Message $message the message to add
* @throws Forum_Topic_Exception
*/
public function addMessage(Forum_Message $message)
{
// this is the current state which is responsible of this action
$this->_state->addAnswer($message);
}
/**
* delete an answer from our topic
* @access public
* @return void
* @param Forum_Message $message the message to delete
* @throws Forum_Topic_Exception
*/
public function deleteMessage(Forum_Message $message)
{
// our current state is responsible of this action
$this->_state->deleteAnswer($message);
}
/**
* retrieve the answer list
* @return array
* @access public
*/
public function & getMessages()
{
return $this->_messages;
}
/**
* lock the topic
* @return void
* @access public
* @throws Forum_Topic_Exception
*/
public function lockTopic()
{
// we notify only the current state that we want to change to another one
$this->_state->lockTopic();
}
/**
* unlock the topic
* @return void
* @access public
* @throws Forum_Topic_Exception
*/
public function unlockTopic()
{
// we notify only the current state that we want to change to another one
$this->_state->unlockTopic();
}
/**
* does our topic have yet answers?
* @access public
* @return boolean
*/
public function haveAnswers()
{
return (is_array($this->_messages) && !empty($this->_messages));
}
/**
* define our topic state
* @access public
* @return void
* @param Forum_Topic_State_Interface $state the state to define
*/
public function setState(Forum_Topic_State_Interface $state)
{
$this->_state = $state;
}
/**
* @access public
* @return string
*/
public function __toString()
{
$output = "does topic have messages?: ".
($this->haveAnswers() ? "1" : "")."\n";
$output .= "does topic is locked?: ".
(($this->_state instanceof Forum_Topic_State_Locked) ? "1" : "")."\n";
$output .= "topic state: ".$this->_state->__toString()."\n";
return $output;
}
}
}
and our answers:
<?php
/**
* $Id$
* @package Poo_Example
* @subpackage State
*/
if (!class_exists('Forum_Message')) {
/**
* this class define a topic answer
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
* @package Poo_Example
* @subpackage State
*/
class Forum_Message {
/**
* constructor
* @access public
*/
public function __construct()
{
}
}
}
To check the good work of our states, we use the following script:
<?php
/**
* $Id$
* @package Poo_Example
* @subpackage State
*/
/**
*
*/
define('__EXAMPLE_PATH__', realpath(dirname(__FILE__).'/../../'));
require_once __EXAMPLE_PATH__ . '/Autoload.php';
/**
* our topic in its differents states
* @package Poo_Example
* @subpackage State
*/
$topic = new Forum_Topic();
echo "-- step 1: topic creation\n";
echo "\n";
echo $topic;
echo "\n";
echo "-- step 2: trying to delete an answer\n";
try {
$topic->deleteMessage(new Forum_Message());
} catch (Exception $e) {
echo "exception thrown => ".$e->getMessage()."\n";
}
echo "-- step 3: adding an answer\n";
$message = new Forum_Message();
try {
$topic->addMessage($message);
} catch(Exception $e) {
echo "exception thrown => ".$e->getMessage()."\n";
}
echo "\n";
echo $topic;
echo "\n";
echo "-- step 4: lock the topic\n";
try {
$topic->lockTopic();
} catch(Exception $e) {
echo "exception thrown => ".$e->getMessage()."\n";
}
echo "\n";
echo $topic;
echo "\n";
echo "-- step 5: adding an answer\n";
try {
$topic->addMessage(new Forum_Message());
} catch(Exception $e) {
echo "exception thrown => ".$e->getMessage()."\n";
}
echo "\n";
echo $topic;
echo "\n";
echo "-- step 6: unlock the topic\n";
try {
$topic->unlockTopic();
} catch(Exception $e) {
echo "exception thrown => ".$e->getMessage()."\n";
}
echo "\n";
echo $topic;
echo "\n";
echo "-- step 7: delete the previous answer\n";
try {
$topic->deleteMessage($message);
} catch(Exception $e) {
echo "exception thrown => ".$e->getMessage()."\n";
}
echo "\n";
echo $topic;
echo "\n";
?>
And we have the following output:
-- step 1: topic creation
does topic have messages?:
does topic is locked?:
topic state: Forum_Topic_State_WithNoAnswers
-- step 2: trying to delete an answer
exception thrown => this topic have no related message
-- step 3: adding an answer
does topic have messages?: 1
does topic is locked?:
topic state: Forum_Topic_State_WithAnswers
-- step 4: lock the topic
does topic have messages?: 1
does topic is locked?: 1
topic state: Forum_Topic_State_Locked
-- step 5: adding an answer
exception thrown => topic is locked
does topic have messages?: 1
does topic is locked?: 1
topic state: Forum_Topic_State_Locked
-- step 6: unlock the topic
does topic have messages?: 1
does topic is locked?:
topic state: Forum_Topic_State_WithAnswers
-- step 7: delete the previous answer
does topic have messages?:
does topic is locked?:
topic state: Forum_Topic_State_WithNoAnswers
Conclusion
This design pattern will remove usage of multiple if / else usage
Yours controls is done only per one variable, and each state is responsible of possibles actions made
Top
: 




