Состояние (State) — шаблон проектирования

4 февраля 2020
Шаблон предлагает схему кардинального изменения поведения объекта в зависимости от изменения его внутреннего состояния (значений свойств). Это позволяет сделать архитектуру объекта гибкой, снизить количество условных операторов и сократить объём обработок.

ПРИМЕР ЗАДАЧИ: реализовать превращение игрока в различных живых существ.

Код реализации паттерна на языке PHP. Скачать исходники (ZIP, 2 Кб)

Шаблон состоит из трёх элементов:
  • Контекст – класс, поведение объектов которого мы собираемся менять с помощью смены состояния. Контекст содержит свойство-состояние и метод для изменения его значения. В нашей задаче Контекстом является игрок;
  • Состояние – класс, определяющий поведение Контекста. Для каждого вида поведения создаётся свой класс-состояние. Иногда в него может передаваться ссылка на Контекст (как правило, это делается, чтобы текущее Состояние могло переводить Контекст в другое Состояние). Роль Состояния у нас будут выполнять персонажи живых существ: корова, орёл и черепаха (далее – игровые персонажи), в которых превращается игрок;
  • Интерфейс состояния – интерфейс или абстрактный класс, объявляющие свойства и методы Состояния, которые впоследствии будет использовать Контекст.

Теперь перейдём непосредственно к коду.

character.classes.php

Файл содержит классы игровых персонажей:
  1. AbstractCharacter – абстрактный класс игрового персонажа;
  2. FarmemCharacter – персонаж «Фермер». Стандартный игровой персонаж;
  3. CowCharacter – персонаж «Корова»;
  4. EagleCharacter – персонаж «Орёл»;
  5. TurtleCharacter – персонаж «Черепаха».

Рассмотрим классы подробнее.

AbstractCharacter

Данный абстрактный класс игрового персонажа служит для упрощения объявления и поддержки единообразия конкретных классов персонажей. По логике шаблона каждый конкретный класс персонажа представляет состояние игрока (Контекста). 

Класс состоит из следующих свойств и методов (большинство методов абстрактные и должны быть определены в дочерних классах):
  • $name – название персонажа;
  • getName() – возвращает название персонажа;
  • skin() – возвращает скин (графическую оболочку персонажа). Абстрактный метод;
  • move() – движение игрока вперёд. Возвращает размер шага или false, если персонаж не может двигаться вперёд. Абстрактный метод;
  • fly() – полёт игрока. Возвращает скорость взлёта или false, если персонаж не может летать. Абстрактный метод;
  • fight() – удар по противнику. Возвращает силу удара или false, если персонаж не может бить. Абстрактный метод.
/**
 * Абстрактный класс "Игоровой персонаж"
 */
abstract class AbstractCharacter
{
    /**
     * Название персонажа
     * @var string
     */
    protected $name = '';

    /**
     * Возвращает название персонажа
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Возвращает скин (графическую оболочку персонажа)
     * @return string
     */
    abstract public function skin();

    /**
     * Движение вперёд
     * @return int|bool - возвращает размер шага или 'false', если персонаж не может двигаться вперёд
     */
    abstract public function move();

    /**
     * Полёт
     * @return int|bool - возвращает скорость взлёта или 'false', если персонаж не может летать
     */
    abstract public function fly();

    /**
     * Удар по противнику
     * @return int|bool - возвращает силу удара или 'false', если персонаж не может бить
     */
    abstract public function fight();
}

FarmerCharacter 

Класс наследуется от AbstractCharacter и представляет стандартного игрового персонажа – фермера. Новых свойств и методов класс не объявляет, но содержит определения для абстрактных методов skin(), move(), fly() и fight().
/**
 * Персонаж "Фермер"
 */
class FarmerCharacter extends AbstractCharacter
{
    /**
     * Название персонажа
     * @var string
     */
    protected $name = 'Фермер';

    /**
     * Возвращает скин (графическую оболочку персонажа)
     * @return string
     */
    public function skin()
    {
        return 'Скин фермера';
    }

    /**
     * Движение вперёд
     * @return int|bool - возвращает размер шага или 'false', если персонаж не может двигаться вперёд
     */
    public function move()
    {
        return 20;
    }

    /**
     * Полёт
     * @return int|bool - возвращает скорость взлёта или 'false', если персонаж не может летать
     */
    public function fly()
    {
        return false;
    }

    /**
     * Удар по противнику
     * @return int|bool - возвращает силу удара или 'false', если персонаж не может бить
     */
    public function fight()
    {
        return 25;
    }
}

CowCharacter, EagleCharacter и TurtleCharacter

Классы тоже наследуется от AbstractCharacter и представляют игровых персонажей – корову, орла и черепаху соответственно. Как и в случае с фермером, новые свойства и методы классы не объявляют, но содержат определения методов skin(), move(), fly() и fight(). Внутри методов каждого персонажа можно прописывать собственную уникальную логику поведения.

Код класса CowCharacter (у остальных персонажей код классов практически аналогичен):
/**
 * Персонаж "Корова"
 */
class CowCharacter extends AbstractCharacter
{
    /**
     * Название персонажа
     * @var string
     */
    protected $name = 'Корова';

    /**
     * Возвращает скин (графическую оболочку персонажа)
     * @return string
     */
    public function skin()
    {
        return 'Скин коровы';
    }

    /**
     * Движение вперёд
     * @return int|bool - возвращает размер шага или 'false', если персонаж не может двигаться вперёд
     */
    public function move()
    {
        return 10;
    }

    /**
     * Полёт
     * @return int|bool - возвращает скорость взлёта или 'false', если персонаж не может летать
     */
    public function fly()
    {
        return false;
    }

    /**
     * Удар по противнику
     * @return int|bool - возвращает силу удара или 'false', если персонаж не может бить
     */
    public function fight()
    {
        return 40;
    }
}

player.classes.php

Содержит класс Player, представляющий игрока. Класс состоит из следующих свойств и методов:
  • $character – игровой персонаж (объект класса AbstractCharacter). В терминологии шаблона данное свойство содержит Состояние;
  • Конструктор __construct($character = false) – создаёт объект игрока;
  • setCharacter($character) – устанавливает игрового персонажа (изменяет Состояние);
  • getCharacterName() – возвращает название персонажа;
  • getSkin() – возвращает графическую оболочку игрока;
  • moveFront() – выполняет движение игрока вперёд;
  • fly() – поднимает игрока вверх и производит полёт;
  • fight() –  производит удар по противнику.
  • test() –  тестирование игрока (вывод скина, названия персонажа, движение вперёд и т.п).

Обратите внимание, что методы getSkin(), moveFront(), fly() и fight() делегируют выполнение действий классам состояний. Именно в этом и заключается смысл шаблона, когда поведение игрока (Контекста) зависит от установленного персонажа (Состояния).
<?
//Подключаем классы для работы с игоровыми персонажами
require_once(__DIR__.'/character.classes.php');

/**
 * Игрок
 */
class Player
{
    /**
     * Игровой персонаж
     * @var AbstractCharacter
     */
    private $character;

    /**
     * Конструктор. Создаёт объект игрока
     * @param AbstractCharacter $character - игровой персонаж. По умлочанию устанавливается персонаж "Фермер"
     */
    public function __construct($character = false)
    {
        //Установка персонажа
        if ($character == false) {
            $character = new FarmerCharacter();
        }
        $this->setCharacter($character);
    }

    /**
     * Устанавливает игорового персонажа
     * @param AbstractCharacter $character - игровой персонаж
     */
    public function setCharacter($character)
    {
        $this->character = $character;
    }

    /**
     * Возвращает название персонажа
     * @return string
     */
    public function getCharacterName()
    {
        return $this->character->getName();
    }

    /**
     * Возвращает графическую оболочку игрока
     * @return string
     */
    public function getSkin()
    {
        return $this->character->skin();
    }

    /**
     * Движение игрока вперёд
     * @return int|string - возвращает размер шага или строку с сообщением об ошибке
     */
    public function moveFront()
    {
        $moveSize = $this->character->move();
        if ($moveSize === false) {
            return 'персонаж не умеет ходить';
        } else {
            return $moveSize;
        }
    }

    /**
     * Полёт игрока
     * @return int|string - возвращает скорость взлёта/полёта, или строку с сообщением об ошибке
     */
    public function fly()
    {
        $flySize = $this->character->fly();
        if ($flySize === false) {
            return 'персонаж не умеет летать';
        } else {
            return $flySize;
        }
    }

    /**
     * Удар игрока по противнику
     * @return int|string - возвращает силу удара или строку с сообщением об ошибке
     */
    public function fight()
    {
        $fightSize = $this->character->fight();
        if ($fightSize === false) {
            return 'персонаж не умеет бить';
        } else {
            return $fightSize;
        }
    }

    /**
     * Тестирование персонажа
     * @return string - вернёт результат тестирования
     */
    public function test()
    {
        $ar = array();
        $ar[] = 'Персонаж: '.htmlspecialchars($this->getCharacterName());
        $ar[] = 'Скин: '.$this->getSkin();
        $ar[] = 'Движение вперёд: '.$this->moveFront();
        $ar[] = 'Полёт: '.$this->fly();
        $ar[] = 'Удар: '.$this->fight();

        return implode('<br>', $ar).'<br><br>';
    }
}

?>

test.php

Содержит тестовый скрипт, в котором создаётся игрок $player (объект класса Player) и выполняется тестирование стандартного персонажа – фермера, установленного для игрока по умолчанию:
//Создаём игрока и выполняем тестирование действий (выводим название персонажа, его скин, проводим удар, движение и полёт)
$player = new Player();
echo $player->test();

Затем превращаем игрока в корову и выполняем тестирование
//Превращаем игрока в корову, проводим тест
$player->setCharacter(new CowCharacter());
echo $player->test();

Потом превращаем игрока в орла
//Превращаем игрока в орла, проводим тест
$player->setCharacter(new EagleCharacter());
echo $player->test();

И в завершении превращаем в черепаху
//Превращаем игрока в черепаху, проводим тест
$player->setCharacter(new TurtleCharacter());
echo $player->test();

Полный код скрипта test.php
<?
//Подключаем класс для работы с игороком
require_once(__DIR__.'/player.class.php');

//Создаём игрока и выполняем тестирование действий (выводим название персонажа, его скин, проводим удар, движение и полёт)
$player = new Player();
echo $player->test();

//Превращаем игрока в корову, проводим тест
$player->setCharacter(new CowCharacter());
echo $player->test();

//Превращаем игрока в орла, проводим тест
$player->setCharacter(new EagleCharacter());
echo $player->test();

//Превращаем игрока в черепаху, проводим тест
$player->setCharacter(new TurtleCharacter());
echo $player->test();

?>

Результат выполнения скрипта test.php
Персонаж: Фермер
Скин: Скин фермера
Движение вперёд: 20
Полёт: персонаж не умеет летать
Удар: 25

Персонаж: Корова
Скин: Скин коровы
Движение вперёд: 10
Полёт: персонаж не умеет летать
Удар: 40

Персонаж: Орёл
Скин: Скин орла
Движение вперёд: 15
Полёт: 30
Удар: 25

Персонаж: Черепаха
Скин: Скин черепахи
Движение вперёд: 5
Полёт: персонаж не умеет летать
Удар: персонаж не умеет бить