В этой статье мы объясним и продемонстрируем модель цепочки ответственности.
Определение
Цепочка ответственности — это шаблон поведенческого проектирования, который обрабатывает запрос через ряд объектов процессора (обработчики / получатели). Запрос отправляется от одного объекта-обработчика к другому и обрабатывается одним (чистая реализация) или всеми обработчиками. Все обработчики являются частью цепочки.
Два простых примера, содержащих цепную логику:
- лицо, использующее банкомат для получения наличных денег (введите пин-код, сумму, квитанцию);
- вызов службы поддержки (список опций, нажмите кнопки набора номера, следуйте инструкциям).
участники
Шаблон в короткой версии включает в себя:
- Обработчик: определяет интерфейс для обработки запросов. Это может быть абстрактный класс, который дополнительно реализует методы по умолчанию и способ установки преемника в цепочке.
- Многие конкретные объекты-обработчики: обрабатывают запрос и при необходимости предоставляют доступ к наследникам;
CoR также может включать в себя:
— объект Client для выполнения запроса и настройки цепочки;
— объект запроса;
— объект ответа;
— другие шаблоны дизайна.
Шаблон проектирования CoR используется не часто, но его основная логика делает его полезным в нескольких случаях.
CoR полезен, когда:
- обработчик должен быть определен автоматически (например, система регистрации);
- обработчик не может быть известен заранее (например, обработка исключений);
- запрос должен проходить через приоритет определенной цепочки (например, распространение события, распространение команды), или порядок цепочки должен быть динамическим.
Основное использование
CoR часто применяется в сочетании с шаблоном Composite , поэтому легко обрабатывать все обработчики одинаково и отправлять запрос наследникам цепочки.
Ниже приведен базовый пример PHP:
<?php abstract class BasicHandler { /** * @var Handler */ private $successor = null; /** * Sets a successor handler. * * @param Handler $handler */ public function setSuccessor(Handler $handler) { $this->successor = $handler; } /** * Handles the request and/or redirect the request * to the successor. * * @param mixed $request * * @return mixed */ abstract public function handle($request); }
Ниже приведен пример (не полностью реализованный) продолжения с кодом выше:
class FirstHandler extends BasicHandler { public function handle($request) { //provide a response, call the next successor } } // .. code for SecondHandler and ThirdHandler classes .. $firstHandler = new FirstHandler(); $secondHandler = new SecondHandler(); $thirdHandler = new ThirdHandler(); $firstHandler->setSuccessor($secondHandler); $secondHandler->setSuccessor($thirdHandler); $result = $firstHandler->handle($request);
Расширенное использование
В основе этого паттерна лежит гибкость и открытая логика в цепной организации.
Эта гибкость становится понятнее с некоторыми примерами.
Из кода BasicHandler
можно автоматизировать метод handle (с некоторыми ограничениями), перенося его область видимости из BasicHandler
в родительский абстрактный класс.
Хотя приведенный ниже пример не решает все проблемы CoR, он показывает, как шаблон может быть легко реструктурирован и адаптирован к другой логике.
<?php abstract class AdvancedHandler { /** * @var Handler */ private $successor = null; /** * Sets a successor handler, * in case the class is not able to satisfy the request. * * @param Handler $handler */ final public function setSuccessor(Handler $handler) { if ($this->successor === null) { $this->successor = $handler; } else { $this->successor->setSuccessor($handler); } } /** * Handles the request or redirect the request * to the successor, if the process response is null. * * @param string|array $data * * @return string */ final public function handle($request) { $response = $this->process($request); if (($response === null) && ($this->successor !== null)) { $response = $this->successor->handle($request); } return $response; } /** * Processes the request. * This is the only method a child can implements, * with the constraint to return null to dispatch the request to next successor. * * @param $request * * @return null|mixed */ abstract protected function process($request); }
class FirstHandler extends AdvancedHandler { public function process($request) { //do something } } // .. code for SecondHandler and ThirdHandler classes ..
$firstHandler = new FirstHandler(); $secondHandler = new SecondHandler(); $thirdHandler = new ThirdHandler(); //the code below sets all successors through the first handler $firstHandler->setSuccessor($secondHandler); $firstHandler->setSuccessor($thirdHandler); $result = $firstHandler->handle($request);
Приведенный выше пример минимизирует методы внутри конкретного обработчика (увеличивая сцепление ), несмотря на ограничение ненулевого ответа.
Если для каждого запроса необходимо переходить в конец цепочки, легко выполнить рефакторинг метода handle, возвращающего ответ, когда преемник равен нулю, поскольку обработчик последней цепочки не имеет преемника. Также возможно использовать более структурированный объект Response.
Существует несколько модификаций CoR — например, объединение этого шаблона с другими, такими как шаблон «Фабрика» или «Декоратор», для получения постепенного построения / изменения отклика.
Примеры из реальной жизни включают создание автомобиля (шасси, интерьер, экстерьер, покраска, колеса и т. Д.) Или создание веб-страницы (заголовок, текст, содержание и нижний колонтитул).
Я призываю вас изучить другие модификации шаблона.
Далее мы рассмотрим некоторые предложения по общим вопросам CoR, таким как настройка приоритета и ускорение цепочки.
Конфигурирование цепочки
Извлечение логики для настройки обработчиков цепочки помогает поддерживать чистоту кода и позволяет легко вносить изменения.
Используя объект конфигурации с Dependency Injection, мы можем сделать конфигурацию глобальной (например, через файл yaml).
class Client { private $firstHandler; public function setChainOrder($handlersOrder) { //code for setup the chain } public function process($request) { $response = $this->firstHandler->handle($request); return $response; } }
$client = new Client(); $client->setChainOrder($myHanldersOrder); $client->process($request);
Внедрение зависимостей может обеспечить большую гибкость. Например, цепочка может быть настроена каждым объектом запроса, как в примере ниже, с небольшой модификацией кода.
$client = new Client(); $handlersOrder = $request->getHandlersOrder(); $client->setChainOrder($handlersOrder); $client->process($request);
Ускорение цепи
Если CoR нагружен несколькими запросами, а обработчики также сложны — возможно, они создают другие классы для обработки запроса — может быть полезен контейнер службы.
Служебный контейнер (или контейнер внедрения зависимостей ), который управляет созданием объектов, не создавая их каждый раз, помогает улучшить память и производительность.
Если CoR часто получает один и тот же тип запроса, система кэширования полезна для повышения скорости.
Одним из возможных решений является использование реализации шаблона Flyweight для хранения ответов и предоставления сохраненного ответа (если он существует) на идентичные запросы без необходимости повторной обработки цепочки.
В этом примере я добавил простой кэш в класс AdvancedHandler
.
abstract class AdvancedHandler { //code ... final public function handle($request) { $response = $this->cache->getResponse($request); if ($response === null) { $response = $this->handleRequestWithChain($request); } return $response; } final public function handleRequestWithChain($request) { $response = $this->process($request); if (($response === null) && ($this->successor !== null)) { $response = $this->successor->handleRequestWithChain($request); } $this->cache->setResponse($response); return $response; } //code ... }
Последние мысли
Цепочка ответственности может быть очень мощным паттерном. Также возможно реализовать цепочку цепочек, составляющую многомерную структуру.
CoR способствует слабой связи : каждый обработчик может быть изменен (или удален) без значительного влияния на всю структуру, но его гибкость при неправильной конструкции может вызвать проблемы на всех задействованных объектах цепи.
Я советую вам тщательно проанализировать проблему, которую вы пытаетесь решить, прежде чем приступить к реализации шаблона CoR, и обратите внимание на определение обработчика (абстрактный класс или интерфейс), запрос, ответ и их взаимодействие.