22.02 2013

Chain of Responsibility (Цепочка обязанностей)

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

Цепочка обязанностей (Chain of Responsibility) относится к классу поведенческих паттернов. Служит для ослабления связи между отправителем и получателем запроса. При этом сам по себе запрос может быть произвольным.

Паттерн не просто так называется цепочкой обязанностей. По сути это набор обработчиков, которые по очереди получают запрос, а затем решают обрабатывать его или нет. Если запрос не обработан, то он передается дальше по цепочке. Если же он обработан, то паттерн сам решает передавать его дальше или нет.

Структура очень простая

Chain of Responsibility (Цепочка обязанностей)

В исполнении на php эта схема приобретает следующий вид

abstract class AbstractHandler
{
    /**
     * @var AbstractHandler
     */
    protected $_next;

    /**
     * Send request by
     *
     * @param mixed $message
     */
    abstract public function sendRequest($message);

    /**
     * @param \AbstractHandler $next
     */
    public function setNext($next)
    {
        $this->_next = $next;
    }

    /**
     * @return \AbstractHandler
     */
    public function getNext()
    {
        return $this->_next;
    }
}

class ConcreteHandlerA extends AbstractHandler
{
    /**
     * @param mixed $message
     */
    public function sendRequest($message)
    {
        if ($message == 1) {
            echo __CLASS__ . "process this message";
        }
        else {
            if ($this->getNext()) {
                $this->getNext()->sendRequest($message);
            }
        }
    }

}

class ConcreteHandlerB extends AbstractHandler
{
    /**
     * @param mixed $message
     */
    public function sendRequest($message)
    {
        if ($message == 2) {
            echo __CLASS__ . "process this message";
        }
        else {
            if ($this->getNext()) {
                $this->getNext()->sendRequest($message);
            }
        }
    }

}

$handler = new ConcreteHandlerA();
$handler->setNext(new ConcreteHandlerB());
//$handler->getNext()->setNext(...);

$handler->sendRequest(1);
$handler->sendRequest(2);

полный исходник

Мы создаем обработчики, и объединяем их в цепочку. Затем отправляем запрос через первый обработчик. Если сообщение удовлетворяет его требованиям он выводит сообщение на экран, если нет - передает его следующему обработчику.

Обратите внимание, если следующий обработчик не нашелся, то сообщение теряется. Если в вашем случае это критично, то стоит бросить исключение или каким-либо иным способом известить пользователя о проблеме.

Давайте перейдем от абстрактного кода, к какому-либо примеру. Разработаем логгер ошибок с несколькими уровнями критичности. Все ошибки писать в лог, уровня critical будем отправлять на e-mail, а debug выводить на экран.

abstract class Logger
{

    const DEBUG = 1;
    const CRITICAL = 2;
    const NOTICE = 4;

    protected $mask = 0;

    /**
     * @var Logger
     */
    protected $next;

    /**
     * @param $mask
     */
    public function __construct($mask)
    {
        $this->mask = $mask;
    }

    /**
     * @param string $message
     * @param int $priority
     */
    public function message($message, $priority)
    {
        if ($this->mask & $priority) {
            $this->_writeMessage($message);
        }

        if ($this->getNext()) {
            $this->getNext()->message($message, $priority);
        }
    }

    abstract protected function _writeMessage($message);

    /**
     * @param Logger $next
     */
    public function setNext(Logger $next)
    {
        $this->next = $next;
    }

    /**
     * @return Logger
     */
    public function getNext()
    {
        return $this->next;
    }
}

class ConsoleLogger extends Logger
{
    protected function _writeMessage($message)
    {
        echo $message . PHP_EOL;
    }
}

class FileLogger extends Logger
{
    protected function _writeMessage($message)
    {
        $f = fopen("error.log", "a");
        fwrite($f, $message . PHP_EOL);
        fclose($f);
    }
}

class EmailLogger extends Logger
{
    protected function _writeMessage($message)
    {
        mail("[email protected]", "error", $message);
    }
}

$logger = new ConsoleLogger(Logger::NOTICE);
$file = new FileLogger(Logger::CRITICAL | Logger::DEBUG | Logger::NOTICE);
$mail = new EmailLogger(Logger::CRITICAL);

$logger->setNext($file);
$file->setNext($mail);

$logger->message("Notice message", Logger::NOTICE);
$logger->message("Debug message", Logger::DEBUG);
$logger->message("Critical error", Logger::CRITICAL);

в виде файла

В этом примере мы реализовали метод обработки запроса только в абстрактном классе, что позволило написать конкретные обработчики очень простыми и лаконичными. При этом они получились довольно универсальны: если понадобится отправлять на почту не только ошибки уровня critical, но и какие-то другие, то достаточно будет изменить маску. При такой организации кода нам не составит труда добавить новый вид логгера в случае необходимости.

Недостаток предыдущего примера в том, что приходится наследовать от одного объекта, что не всегда удобно. Можно конечно использовать интерфейсы, но тогда количество однотипного кода увеличится. С этой проблемой можно справится с помощью traits, но это довольно усложнит код. Нужно другое решение.

Можно использовать некоторый контейнер, который будет отвечать за:

  • Хранение и удаление хэндлеров
  • Поочередную передачу сообщения каждому хэндлеру

При такой реализации хэндлерам нужно будет лишь реализовать интерфейс обработки сообщения.

Chain of Responsibility (Цепочка обязанностей)

Вот пример

interface HandlerInterface
{
    public function sendRequest($message);
}

class HandlerContainer implements HandlerInterface
{
    /**
     * @var HandlerInterface[]
     */
    protected $handlers = [];

    public function sendRequest($message)
    {
        foreach ($this->handlers as $handler) {
            $handler->sendRequest($message);
        }
    }

    public function addHandler(HandlerInterface $handler)
    {
        $this->handlers[] = $handler;
    }
}

class ConcreteHandler implements HandlerInterface
{
    public function sendRequest($message)
    {
        echo "Hello, " . $message . '!' . PHP_EOL;
    }
}

$handler = new HandlerContainer();
$handler->addHandler(new ConcreteHandler());
// $handler->addHandler(new ConcreteHandlerB());
// ...

$handler->sendRequest("John");

в виде файла

В таком случае хэндлеры не могут прервать цепочку вызовов, но это решается либо возвращаемым булевым значением из хэндлера, либо передачей в хэндлер некоего объекта состояния (например паттерна [командна]), через который можно прервать цепочку вызовов.

Применяйте этот паттерн, если

  • несколько объектов могут обработать сообщение
  • вы не хотите явно указывать, кто обрабатывает сообщение
  • набор объектов, которые способны обработать запрос задается в процессе выполнения

Реальным примером, где этот паттерн был применен может быть практически любой event manager. Будь то стандартные события в Java Script или новая система событий в Zend Frameword 2.

comments powered by Disqus