Описывается поведенческий шаблон проектирования State, условия применения, приведен пример реализации.
Содержание
- Применение
- Реализация
- Итоги
Применение
Думаю всем знакома ситуация, когда дальнейшие действия некоего объекта зависят от его внутреннего состояния. А что такое состояние? Это конкретный набор значений полей. Если состояний немного, то это не страшно, выручает условный оператор. Но подобное встречается нечасто. Да и свойства, а значит и возможные состояния, по мере развития проекта имеют гнусное свойство размножаться. А если свойств изначально немало, то при изменении любого из них требуется множество if или switch, чтобы определиться с дальнейшими действиями. Код получается сложным и объемным, в итоге его сопровождение превращается в тяжкий, чреватый ошибками, труд.
В этом случае может помочь шаблон проектирования State. Его суть в том, что определяется набор возможных состояний объекта, назовем его Context. Эти состояния представляются самостоятельными классами, которым передается ссылка на исходный объект. Такому классу не надо определять свое состояние, поэтому он четко знает, как обработать команду. В итоге, если теперь Context в состоянии Х получает какую-то команду, то он делегирует реакцию в класс с состоянием Х, который однозначно знает, что надо делать. В результате клубок из if и switch трансформируется в набор относительно простых и конкретных объектов, реакцию которых легко изменять. При появлении же нового состояния надо просто добавить новый класс состояния, а не править код всего класса.
Реализация
В примере шаблона используются:
- Context — исходный класс, определяет интерфейс взаимодействия с внешним кодом. Его реакция на команды зависит от его состояния. Это состояние определяется ссылкой на объект текущего состояния;
- АState — абстрактный класс, от которого наследуются классы конкретных состояний исходного класса. Его интерфейс повторяет интерфейс
Context
; - IName — вспомогательный интерфейс классов состояний, для реализации шаблона не обязателен. В примере используется исключительно при выдачи сообщений;
- StateN — набор классов, представляющих текущие состояния класса Context.
Начнем с класса, взаимодействующего с клиентом. Он может отрабатывать две команды request1 и request2.
Class Context
{
private $state; //ссылка на текущее состояние класса
public function __construct()
{
$this->state = new StateA(); //инициализация начального состояние
$this->changeState($state);
}
public function changeState(AState $state)
{
$this->state = $state;
$this->state->setContext($this);
$this->state->setName();
$str = 'Объект Context перешел в состояние ' . $this->state->getName() . '<br>';
return $str;
}
public function request1(): void
{
$this->state->handle1();
}
public function request2(): void
{
$this->state->handle2();
}
}
Как уже говорилось интерфейс IName вспомогательный и используется только для выдачи внятного лога на клиент.
interface IName
{
public function setName();
public function getName();
}
Наши будущие классы состояния являются наследниками абстрактного класса AState. Обратите внимание, класс повторяет интерфейсные методы класса Context.
abstract class AState
{
protected $context;
public function setContext(Context $context)
{
$this->context = $context;
}
abstract public function setName();
abstract public function handle1(): void;
abstract public function handle2(): void;
}
Условимся, что объект Context будет иметь два состояния, которые определяются классами StateA и StateB.
class StateA extends AState implements IName
{
private $name;
public function getName() {
return $this->name;
}
public function setName() {
$this->name = 'StateA';
}
public function handle1(): void
{
$str1 = 'Объект Context в состоянии ' . $this->getName() . ' обрабатывает команду "Изменить контекст" <br>';
$str2 = $this->context->changeState(new StateB());
echo $str1 . $str2;
}
public function handle2(): void
{
echo 'Объект Context в состоянии ' . $this->getName() . ' обрабатывает запрос request2 <br>';
}
}
class StateB extends AState implements IName
{
public function getName() {
return $this->name;
}
public function setName() {
$this->name = 'StateB';
}
public function handle1(): void
{
echo 'StateB handles request1';
}
public function handle2(): void
{
$str1 = 'Объект Context в состоянии ' . $this->getName() . ' обрабатывает запрос request2<br>';
$str2 = $this->context->changeState(new StateA());
echo $str1 . $str2;
}
}
Для демонстрации работы шаблона State используется следующий код. Чтобы не повторяться, определимся с шапкой web страниц.
<!DOCTYPE html>
<?php
$css = '/style.css';
?>
<html>
<head>
<title>Demo example of pattern state</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="<?php echo $css; ?>">
<link type="image/x-icon" href="/favicon.ico" rel="shortcut icon">
</head>
А теперь собственно страница, запускающая обработку.
<?php
// Реализация работы шаблона проектирования State
include 'header.php';
?>
<body>
<div class="wrap">
<h3>Демо пример реализации шаблона State</h3>
<p>
<a href="app.php" target="_self">Изменить состояние</a>
</div>
</body>
</html>
Следующий скрипт реализует обработку объекта Context, которая состоит из 3 команд.
<?php
include 'AState.php';
include 'IName.php';
include 'StateA.php';
include 'StateB.php';
include 'Context.php';
$context = new Context();
$context->request1();
$context->request2();
$context->request1();
// сделал кнопка возврата к предыдущей странице, вдруг кто-то захочет поиграться :)
echo '<p><input type="button" onclick="history.back();" value="Вернуться"/></p>';
В итоге на клиент выдается следующий лог о проделанной работе.
Итоги
Итак, сегодня был рассмотрен поведенческий шаблон проектирования State. Можно констатировать, что шаблон State целесообразно применять для объектов, поведение которых зависит от их текущего состояния. Шаблон позволяет упростить код за счет замены множества условных операторов набором простых объектов.
К недостаткам шаблона следует отнести зависимости между классами состояний в том случае, если объект Context в результате отработки команды должен перейти в другое состояние. Например, в метод handle1() класса StateA зависит от объекта класса StateB.
Перейти к списку шаблонов проектирования.