Представлен поведенческий шаблон проектирования State, условия его применения, приведен пример реализации шаблона.
Содержание
- Применение
- Реализация
- Итоги
Применение
Многим знакома ситуация, когда дальнейшие действия некоего объекта зависят от его внутреннего состояния. А что такое состояние? Это конкретный набор значений его полей. Если состояний немного, то выручает условный оператор. В нем перебираются возможные состояния, когда найдено текущее состояние, выполняется соответствующая ему обработка. Но часто состояний много. Да и их количество по мере развития проекта имеет гнусное свойство размножаться. А если важных параметров изначально немало, то ваш проект неизбежно обрастёт длинным перечнем условий, чтобы определиться с дальнейшими действиями. Код получается сложным и объемным, и в итоге сопровождение приложения превращается в тяжкий, чреватый ошибками, труд.
В таких случаях вас может выручить шаблон проектирования State. Его суть в том, что в объекте, осуществлющем обработку, определяется набор возможных состояний. Каждое из этих состояний представляется самостоятельным классом, а основной обработчик, назовём его контекст Context, хранит ссылку на класс, соответствующий текущему состоянию. У класса состояние однозночно определено и не меняется, поэтому он четко знает, что надо делать. В итоге, если теперь объект класса Context в состоянии Х получает какую-то команду, то он делегирует обработку в объект с состоянием Х, ссылка на который у него хранится. В результате клубок из if или switch основного обработчика трансформируется в набор менее сложных самостоятельных объектов. При добавлении нового состояния надо просто добавить новый класс состояния, а не править многочисленные if или switch. Также, если понадобится изменить алгоритм существующей команды, вам надо будет доработать логику отдельного класса состояния, не затрагивая остальной код.
Реализация
Ниже будет создан класс, экземпляр которого будет управлять двигателем, включать, изменять скорость оборотов и выключать. Но вначале небольшие пояснения. В примере используются:
- Context — класс основного обработчика, взаимодействующий с клиентом. Его реакция на запросы зависит от состояния двигателя. Это состояние определяется ссылкой;
- INewState — содержит логику определения следующего состояния управляемого объекта. Поскольку переход обработчика Context из одного состояния в другое может достаточно часто изменяться при разработке или доработках приложения, эта логика выделена в отдельный интерфейс;
- АState — абстрактный класс, от которого наследуются классы конкретных состояний объекта управления. Набор его команд должен совпадать с набором команд, которые должен отрабатывать класс
Context; - State… — набор классов, представляющих возможные состояния класса Context, наследники класса АState. Именно они и отрабатывают поступающие команды. Ссылки на экземпляры именно этих классов содержит Context.
Класс Context
Класс Context взаимодействует с клиентом, реализует интерфейс INewState. Чтобы не усложнять пример, интерфейс будет состоять из одного метода. Входным параметром метода является наименование вновь поступившей команды.
interface INewState
{
public function getNewState(string $command): AState;
}
Объект класса Context принимает на исполнение команды и делегирует их выполнение одному из объектов, представляющим текущее состояние.
Для принятия решения по переходу в другое состояние используется метод getNewState(), задекларированный в интерфейсе INewState. Здесь стоит заметить следующее. Во-первых, в каждом состоянии возможно разное количество переходов в другие состояния, это определяется бизнес-логикой. Во-вторых, логично предположить, что переход в другое состояние возможен лишь в случае успешной отработки поступившей команды, поэтому для перехода из текущего в другое состояние используются лишь успешно выполненные команды, иначе объект управления остаётся в прежнем состоянии.
Абстрактный класс AState является родительским для классов состояний. Но, поскольку он часто фигурирует в Contex’е, его код приведен в этом разделе. Он декларирует методы исполнения поступающих команд, фактически это набор команд, которые должен отрабатывать обработчик класса Context.
/**
* Абстрактное состояние
*/
abstract class AState
{
abstract public function setOn(): bool;
abstract public function setInc(): bool;
abstract public function setDec(): bool;
abstract public function setOff(): bool;
abstract public function getName(): string;
}
Вот теперь основной обработчик команд. Его задача принять от клиента команду и передать ее на исполнение требуемому исполнителю. Чтобы знать, кому передавать исполнение поступившей команды, у него есть ссылка на объект текущего состояния. Также он отслеживает состояние управляемого объекта и при изменении его состояния обновляет ссылку текущего состояния.
/**
* Context
*/
class Context implements INewState
{
private AState $state;
public function __construct(AState $state)
{
$this->state = $state; // инициализация текущего состояния
}
public function changeState(AState $state): string
{
$this->state = $state;
return 'Context перешёл в состояние: ' . $this->state->getName() . PHP_EOL;
}
public function getCurrentStateName(): string
{
return $this->state->getName();
}
public function setOn(): void
{
if ($this->state->setOn()) {
$this->changeState($this->getNewState('setOn'));
}
}
public function setInc(): void
{
if ($this->state->setInc()) {
$this->changeState($this->getNewState('setInc'));
}
}
public function setDec(): void
{
if ($this->state->setDec()) {
$this->changeState($this->getNewState('setDec'));
}
}
public function setOff(): void
{
if ($this->state->setOff()) {
$this->changeState(new StateOff());
}
}
// определяет следующее состояние объекта и обновляет свою ссылку текущего состояния
public function getNewState(string $command): AState
{
return match ($this->state->getName()) {
'off' => $command === 'setOn' ? new StateMin() : $this->state,
'min' => $command === 'setInc' ? new StateMiddle() :
($command === 'setOff' ? new StateOff() : $this->state),
'middle' => $command === 'setInc' ? new StateMax() :
($command === 'setDec' ? new StateMin() :
($command === 'setOff' ? new StateOff() : $this->state)),
'max' => $command === 'setDec' ? new StateMiddle() :
($command === 'setOff' ? new StateOff() : $this->state),
default => $this->state,
};
}
}
Классы Состояний
Теперь создадим классы состояний. Они наследуются от абстрактного класса AState.
В нашем случае возможных состояний будет четыре, т.е. требуются четыре класса. Команды управления возвращают булево значение: при успешно отработанной команде возвращается true, а если в данном состоянии команда недопустима или произошла ошибка при исполнении команды, вернется false. Для простоты примера логика команд намеренно максимально упрощена.
Класс исходного состояния объекта управления. Из выключенного состояния объект может перейти только на минимальные обороты.
class StateOff extends AState
{
private string $name = 'off';
public function setOn(): bool
{
$res = true;
try {
//отработка команды на включение
} catch (Exception $exc) {
$res = false;
//делаем запись в лог об ошибке при выполнении команды
}
return $res;
}
public function setInc(): bool { return false; }
public function setDec(): bool { return false; }
public function setOff(): bool { return false; }
public function getName(): string { return $this->name; }
}
По команде включения двигатель переходит в состояние минимальных оборотов, представленное следующим классом.
class StateMin extends AState
{
private string $name = 'min';
public function setOn(): bool { return false; }
public function setInc(): bool
{
$res = true;
try {
//отработка команды на увеличение оборотов
} catch (Exception $exc) {
$res = false;
//делаем запись в лог об ошибке при выполнении команды
}
return $res;
}
public function setDec(): bool { return false; }
public function setOff(): bool
{
$res = true;
try {
//отработка команды на выключение
} catch (Exception $exc) {
$res = false;
//делаем запись в лог об ошибке при выполнении команды
}
return $res;
}
public function getName(): string { return $this->name; }
}
Данный класс представляет состояние двигателя на средних оборотах. Из этого состояния объект может перейти в три другие состояния.
class StateMiddle extends AState
{
private string $name = 'middle';
public function setOn(): bool { return false; }
public function setInc(): bool
{
$res = true;
try {
//отработка команды на увеличение оборотов
} catch (Exception $exc) {
$res = false;
//делаем запись в лог об ошибке при выполнении команды
}
return $res;
}
public function setDec(): bool
{
$res = true;
try {
//отработка команды на уменьшение оборотов
} catch (Exception $exc) {
$res = false;
//делаем запись в лог об ошибке при выполнении команды
}
return $res;
}
public function setOff(): bool
{
$res = true;
try {
//отработка команды на выключение генератора
} catch (Exception $exc) {
$res = false;
//делаем запись в лог об ошибке при выполнении команды
}
return true;
}
public function getName(): string { return $this->name; }
}
Класс, представляющий состояние максимальных оборотов.
class StateMax extends AState
{
private string $name = 'max';
public function setOn(): bool { return false; }
public function setInc(): bool { return false; }
public function setDec(): bool
{
$res = true;
try {
//отработка команды на уменьшение оборотов
} catch (Exception $exc) {
$res = false;
//делаем запись в лог об ошибке при выполнении команды
}
return $res;
}
public function setOff(): bool
{
$res = true;
try {
//отработка команды на выключение генератора
} catch (Exception $exc) {
$res = false;
//запись в лог об ошибке при выполнении команды
}
return $res;
}
public function getName(): string { return $this->name; }
}
Это всё.
Одно замечание.
В посте стояла задача показать логику шаблона проектирования и взаимосвязи его компонентов, а не предоставить вылизанный оптимальный код, поэтому предложеные скрипты далеко не оптимальны. Например, надо бы реализовать часть методов в абстрактном классе, а в наследниках, там где нужно, переопределять их. Ну что ж, это уже ваша часть работы 🙂
UML диаграмма классов приведенного примера наглядно отображает взаимосвязи компонентов шаблона State.
Итоги
Итак, был рассмотрен поведенческий шаблон проектирования State. Можно констатировать, что шаблон State применяется для управления объектов, поведение которых зависит от их текущего состояния. Применение шаблона позволяет упростить код за счет замены множества условных операторов набором простых объектов. За счет этого в дальнейшем упрощается доработка проекта при изменении бизнес-логики, например, при введении в объект новых состояний, изменении действующих алгоритмов.
К недостаткам шаблона следует отнести зависимости между классами состояний в том случае, если объект Context в результате отработки команды должен перейти в другое состояние.
Перейти к списку шаблонов проектирования.
