15.11 2013

Command (Команда)

Перед прочтением ознакомьтесь с введением в паттерны проектирования на PHP, в котором описаны принятые соглашения и понятия. Данная статья дополняется с некоторой периодичностью, так что если вы ее читали ранее, не факт что данные не изменились.

Команда (Command) относится к классу поведенческих паттернов. Команда представляет собой некоторое действие и его параметры. Суть паттерна в том, чтобы отделить инициатора и получателя команды.

Этот паттерн широко используется в C# и Java для обработки событий возникающих в форме (GUI). Так как на PHP никто не занимается разработкой GUI приложений, то приводить подобный код я не стану. Но не стоит отчаиваться, в PHP он нашел свое место.

Структура

Command (Команда)

Структура довольно простая. 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) становится слишком много, а полиморфизм не в силах помочь.
  • Команда может быть иерархична, т.е. вызов команды вызывает какую-то другую команду в зависимости от условий. Иногда полезно делать команду вместе с паттерном компоновщик
  • Если ваша команда делегирует выполнение другой команде, то это уже цепочка обязанностей
  • В некоторых случаях в команде объявляют антидействие, которое приводит все в начальное состояние. В таком случае получается паттерн хранитель
comments powered by Disqus