Паттерн проектирования Command (Команда) на PHP
Перед прочтением ознакомьтесь с введением в паттерны проектирования на PHP, в котором описаны принятые соглашения и понятия. Данная статья дополняется с некоторой периодичностью, так что если вы ее читали ранее, не факт что данные не изменились.
Команда (Command) относится к классу поведенческих паттернов. Команда представляет собой некоторое действие и его параметры. Суть паттерна в том, чтобы отделить инициатора и получателя команды.
Этот паттерн широко используется в C# и Java для обработки событий возникающих в форме (GUI). Так как на PHP никто не занимается разработкой GUI приложений, то приводить подобный код я не стану. Но не стоит отчаиваться, в PHP он нашел свое место.
Структура
Структура довольно простая. Dispatcher посылает сообщение (команду), при этом он не знает, кто эту команду получит. Это сообщение проходит через ConcreteCommand и попадает в Receiver. При этом Receiver не знает, от кого это сообщение пришло. Получается, что в этой диаграмме никто не обладает полными знаниями о том что происходит. Но этими знаниями обладает тот, кто подготовит всю эту цепочку для использования.
Для того что бы разобрать назначение участников этого паттерна, разберем известный и абсолютно отрешенный от реальности пример с лампочкой и выключателем.
Постановка задачи: разработать программу, которая будет принимать один аргумент из командной строки. Если передать ON - лампочка включается, если OFF - выключается.
Для начала сделаем класс лампы:
class Lamp
{
public function turnOn()
{
echo "I'm bright and cheerful light.\n";
}
public function turnOff()
{
echo "I am quiet and peaceful shadow\n";
}
}
И если сейчас мы захотим решить эту задачу без использования паттерна команда, то наш код будет выглядеть как-то так:
$lamp = new Lamp();
if ($argv[1] == 'ON') {
$lamp->turnOn();
} else {
if ($argv[1] == 'OFF') {
$lamp->turnOff();
} else {
throw new \RuntimeException('I understand only ON/OFF command');
}
}
И несмотря на то, что задача в общем-то решена, чувствуется некая неудовлетворенность. В основном из-за того, что каждое расширение требований изначальной задачи будет сопровождаться увеличением вложенности IF:
$lamp = new Lamp();
if ($argv[1] == 'ON') {
$lamp->turnOn();
} else {
if ($argv[1] == 'OFF') {
$lamp->turnOff();
}
else {
if ($argv[1] == 'SOS') {
while(true) {
$lamp->turnOn(); sleep(0.1); $lamp->turnOff(); sleep(0.1);
$lamp->turnOn(); sleep(0.1); $lamp->turnOff(); sleep(0.1);
$lamp->turnOn(); sleep(0.1); $lamp->turnOff(); sleep(0.1);
$lamp->turnOn(); sleep(0.5); $lamp->turnOff(); sleep(0.1);
$lamp->turnOn(); sleep(0.5); $lamp->turnOff(); sleep(0.1);
$lamp->turnOn(); sleep(0.5); $lamp->turnOff(); sleep(0.1);
}
}
else {
if ($argv[1] == 'TIMEOUT') {
$lamp->turnOn();
sleep($arv[2]);
$lamp->turnOff();
}
else {
// ...
}
}
}
}
Уже не так аккуратно и эффектно. Пришло время посмотреть, как с этой задачей можно справится с помощью паттерна команда.
Приведенный выше класс Lamp будет играть роль Receiver. Он останется в полностью нетронутом виде, что бы показать, что класс Receiver не обязан ничего знать.
Определим интерфейс команды. Командами будут все варианты использования лампы в примере выше.
interface CommandInterface {
public function execute();
}
class TurnOnCommand implements CommandInterface {
protected $lamp;
public function __construct(Lamp $lamp) {
$this->lamp = $lamp;
}
public function execute() {
$this->lamp->turnOn();
}
}
class TurnOffCommand implements CommandInterface {
protected $lamp;
public function __construct(Lamp $lamp) {
$this->lamp = $lamp;
}
public function execute() {
$this->lamp->turnOff();
}
}
В целях экономии места, не стану здесь приводить все команды, а только две.
- TurnOnCommand - команда включения лампы
- TurnOffCommand - команда выключения лампы
В данном случае команды выступают в роли тонких делегатов, передавая право выполнять команду объекту класса Lamp
Уже сейчас их можно использовать
$lamp = new Lamp();
$on = new TurnOnCommand($lamp);
$on->execute(); // напечатает: I'm bright and cheerful light.
Понятное дело, что в таком виде их использовать неудобно. Давайте абстрагируемся от создания команды с помощью паттерна фабричный метод.
class LampCommandFactory
{
public function factory($type, Lamp $lamp)
{
if ($type == 'ON') {
return new TurnOnCommand($lamp);
}
if ($type == 'OFF') {
return new TurnOffCommand($lamp);
}
throw new RuntimeException('Cannot find command ' . $type);
}
}
// ...
$lamp = new Lamp();
$factory = new LampCommandFactory(); // создаем фабрику
// фабрика вернет нам нужную команду
$factory->factory($argv[1], $lamp)->execute();
В такой реализации есть еще что улучшить. Например команду TimeoutCommand будет довольно проблематично добавить в фабрику, так как для этой команды требуется дополнительный параметр $timeout, который мы получаем из командной строки.
Что бы разрешить эту проблему, вместо фабрики используем регистр:
class CommandRegistry {
private $registry = [];
public function add(CommandInterface $command, $type) {
$this->registry[$type] = $command;
}
public function get($type) {
if (!isset($this->registry[$type])) {
throw new RuntimeException('Cannot find command ' . $type);
}
return $this->registry[$type];
}
}
// ...
$lamp = new Lamp();
$registry = new CommandRegistry();
$registry->add(new TurnOnCommand($lamp), 'ON');
$registry->add(new TurnOffCommand($lamp), 'OFF');
$registry->add(new SosCommand($lamp), 'SOS');
$registry->add(new TimeoutCommand($lamp, $argv[2]), 'TIMEOUT');
$registry->get($argv[1])->execute();
В целом оба эти варианта имеют право на жизнь, преимущество первого - ленивая инициализация команд. Преимущество второго - упрощенная конфигурация.
Наилучшей демонстрацией реального применения будет, пожалуй, компонент Symfony Console. Познакомится с которым вам поможет документация. Они пошли по второму пути.
Применимость
Применяйте этот паттерн, если
- вам необходимо отделить инициатора вызова от исполнителя
- логический операторов (if, switch) становится слишком много, а полиморфизм не в силах помочь.
Другие паттерны
- Команда может быть иерархична, т.е. вызов команды вызывает какую-то другую команду в зависимости от условий. Иногда полезно делать команду вместе с паттерном компоновщик
- Если ваша команда делегирует выполнение другой команде, то это уже цепочка обязанностей
- В некоторых случаях в команде объявляют антидействие, которое приводит все в начальное состояние. В таком случае получается паттерн хранитель