Имя
«Паттерн Chain of Responsibility»
Цепочка обязанностей - паттерн поведения, выстраивающий объекты составных частей приложения связанными между собой по цепочке, для передачи запроса на обработку от более низких, детализированных слоев системы к более высоким глобальным.
Условия, Задача, Назначение
Позволяет избежать привязки отправителя запроса к его получателю, давая шанс обработать запрос нескольким объектам. Связывает объекты-получатели в цепочку и передает запрос вдоль этой цепочки, пока его один из получателей его не обработает.
Возникает очень гибкая и свободная схема процесса обработки какого-либо запроса: в глобальном объекте какого-нибудь самого верхнего уровня можно определить алгоритм обработки по-умолчанию (в результате чего система уже может быть запущена, только все запросы вне зависимости от их содержания и контекста будут обрабатываться единым способом), а потом лишь там где требуется специальное обслуживание запроса - просто добавлять реализацию методов обработки у соответствующих более низких и детализированных слоев системы.
Мотивация
Рассмотрим контекстно-зависимую оперативную справку в графическом интерфейсе пользователя, который может получить дополнительную информацию по любой части интерфейса, просто щелкнув на ней мышью. Содержание справки зависит от того, какая часть интерфейса и в каком контексте выбрана. Например, справка по кнопке в диалоговом окне может отличаться от справки по аналогичной кнопке в главном окне приложения. Если для некоторой части интерфейса справки нет, то система должна показать информацию о ближайшем контексте, в котором она находится, например, о диалоговом окне в целом.
Поэтому естественно было бы организовать возможность обработки справочной информации при следовании от более конкретных разделов к более общим. Кроме того, ясно, что запрос на получение справки обрабатывается одним из нескольких объектов пользовательского интерфейса, каким именно - зависит от контекста и имеющейся в наличии информации.
Проблема в том, что объект, инициирующий запрос (например, кнопка), не располагает информацией о том, какой объект в конечном итоге предоставит справку.
Нам необходим какой-то способ отделить кнопку-инициатор запроса от объектов, владеющих справочной информацией. Как этого добиться, показывает
паттерн цепочка обязанностей.
Идея заключается в том, чтобы разорвать связь между отправителями и получателями, дав возможность обработать запрос нескольким объектам. Запрос перемещается по цепочке объектов, пока один из них не обработает его.
Первый объект в цепочке получает запрос и либо обрабатывает его сам, либо направляет следующему кандидату в цепочке (расположенному на более верхнем уровне иерархии и обычно включающему в себя сам этот предыдущий объект), который ведет себя точно так же.
У объекта, отправившего запрос, отсутствует информация об обработчике. Мы только можем сказать, что у запроса есть анонимный получатель (implicit receiver).
Предположим, что пользователь запрашивает справку по кнопке Print (печать). Она находится в диалоговом окне PrintDialog, содержащем информацию об объекте приложения, которому принадлежит (см. предыдущую диаграмму объектов). На представленной диаграмме взаимодействий показано, как запрос на получение справки перемещается по цепочке.
В данном случае ни кнопка aPrintButton, ни окно aPrintDialog не обрабатывают запрос, он достигает объекта anApplication, который может его обработать или игнорировать. У клиента, инициировавшего запрос, нет прямой ссылки на объект, который его в конце концов выполнит.
Чтобы отправить запрос по цепочке и гарантировать анонимность получателя, все объекты в цепочке имеют единый интерфейс для обработки запросов и для доступа к своему преемнику (следующему объекту в цепочке). Например, в системе оперативной справки можно было бы определить класс HelpHandler (предок классов всех объектов-кандидатов) с операцией HandleHelp. Тогда классы, которые будут обрабатывать запрос, смогут его передать своему родителю.
Для обработки запросов на получение справки классы Button,
Dialog и
Application пользуются операциями
HelpHandler. По умолчанию операция
HandleHelp просто перенаправляет запрос своему преемнику. В подклассах эта операция замещается, так что при благоприятных обстоятельствах может выдаваться справочная информация. В противном случае запрос отправляется дальше посредством реализации по умолчанию.
Признаки применения, использования паттерна Цепочка обязанностей (Chain of Responsibility)
Используйте цепочку обязанностей, когда:
- Есть более одного объекта, способного обработать запрос, при этом настоящий обработчик заранее неизвестен и должен быть найден в соответствие с какой-либо кастомной логикой.
- Нужно отправить запрос одному из нескольких объектов, не указывая явно, какому именно.
- Набор объектов, способных обработать запрос, должен определяться динамически.
Типичная структура цепочки:
Участники паттерна Цепочка обязанностей (Chain of Responsibility)
- Handler (HelpHandler) – обработчик.
Определяет интерфейс для обработки запросов.
Реализует связь с преемником (необязательно).
- ConcreteHandler (PrintButton, PrintDialog) - конкретный обработчик.
Обрабатывает запрос, за который отвечает.
Имеет доступ к своему преемнику.
Если ConcreteHandler способен обработать запрос, то так и делает, если не может, то направляет его - его своему преемнику вверх по цепочке.
- Client – клиент.
Отправляет запрос некоторому объекту ConcreteHandler в цепочке.
Схема использования паттерна Цепочка обязанностей (Chain of Responsibility)
Когда клиент инициирует запрос, он начинает продвигаться по цепочке, пока некоторый объект
ConcreteHandler не возьмет на себя ответственность за его обработку.
Вопросы, касающиеся реализации паттерна Цепочка обязанностей (Chain of Responsibility)
При рассмотрении цепочки обязанностей следует обратить внимание на следующие моменты:
- Реализация цепочки преемников.
Есть два способа реализовать такую цепочку: определить новые связи (обычно это делается в классе Handler, но можно и в ConcreteHandler), либо использовать существующие.
До сих пор в наших примерах определялись новые связи, однако можно воспользоваться уже имеющимися ссылками на объекты для формирования цепочки преемников. Например, ссылка на родителя в иерархии «часть-целое» (паттерн компоновщик) может заодно определять и преемника «части». В структуре виджетов такие связи тоже могут существовать. В разделе, посвященном паттерну компоновщик, ссылки на родителей обсуждаются более подробно.
Существующие связи можно использовать, когда они уже поддерживают нужную цепочку. Тогда мы избежим явного определения новых связей и сэкономим память. Но если структура не отражает устройства цепочки обязанностей, то уйти от определения избыточных связей не удастся.
- Соединение преемников.
Если готовых ссылок, пригодных для определения цепочки, нет, то их придется ввести. В таком случае класс Handler не только определяет интерфейс запросов, но еще и хранит ссылку на преемника. Следовательно у обработчика появляется возможность определить реализацию операции HandleRequest по умолчанию - перенаправление запроса преемнику (если таковой существует). Если подкласс ConcreteHandler не заинтересован в запросе, то ему и не надо замещать эту операцию, поскольку по умолчанию запрос как раз и отправляется дальше. Вот пример базового класса HelpHandler, в котором хранится указатель на преемника:package chainOfResponsibility;
public class HelpHandler {
HelpHandler successor;
public HelpHandler(HelpHandler successor) {
super();
this.successor = successor;
}
public void HandleHelp() {
if (successor != null) {
successor.HandleHelp();
}
}
}
- Представление запросов.
Представлять запросы можно по-разному. В простейшей форме, например в случае класса HandleHelp, запрос жестко кодируется как вызов соответствующего дополнительного метода этого класса. Это удобно и безопасно, но переадресовывать тогда можно только фиксированный набор запросов, для которых в классе Handler определены специальные методы.
Альтернатива - использовать единую функцию-обработчик, которой передается код запроса (скажем, целое число или строка). Так можно поддержать заранее неизвестное число запросов. Единственное требование состоит в том, что отправитель и получатель должны договориться о способе кодирования запроса.
Это более гибкий подход, но при реализации нужно использовать условные операторы для распределения запросов по коду их обработки. Кроме того, не существует безопасного с точки зрения типов способа передачи параметров, поэтому упаковывать и распаковывать их приходится вручную. Очевидно, что это не так безопасно, как прямой вызов операции.
Чтобы решить проблему передачи параметров, допустимо использовать отдельные объекты-запросы, в которых инкапсулированы параметры запроса. Т.е. специальный класс Request, например, может представлять некоторые запросы явно, а их новые типы описываются в подклассах. Подкласс может определить другие параметры. Обработчик должен иметь информацию о типе запроса (какой именно подкласс Request используется), чтобы разобрать эти параметры.
Для идентификации запроса в классе Request можно определить функцию доступа, которая возвращает идентификатор класса. Вместо этого получатель мог бы воспользоваться информацией о типе, доступной во время выполнения, если язык программирования поддерживает такую возможность (в случае Java-ы для этих целей можно б было использовать результат выполнения .getClass().getName()).
Приведем пример функции диспетчеризации, в которой используются объекты для идентификации запросов. Операция GetKind, указанная в базовом классе Request, определяет вид запроса:
package chainOfResponsibility;
public void HandleRequest
(Request req
) { switch (req.GetKind()) {
HandleHelp(req);
break;
HandlePrint(req);
break;
default:
// ...
break;
}
}
protected void HandlePrint
(Request req
) { // ...
}
protected void HandleHelp
(Request req
) { // ... }
}
И подклассы могут расширить схему диспетчеризации, переопределив операцию HandleRequest. Подкласс обрабатывает лишь те запросы, в которых заинтересован, а остальные отправляет родительскому классу. В этом случае подкласс именно расширяет, а не замещает операцию HandleRequest. Например, подкласс ExtendedHandler расширяет операцию HandleRequest, определенную в классе Handler, следующим образом:
package chainOfResponsibility;
public class ExtendedHandler
extends Handler {
public void HandleRequest
(Request req
) { switch (req.GetKind()) {
HandlerPreview(req);
break;
default:
super.HandleHelp(req);
break;
}
}
protected void HandlerPreview
(Request req
) { // ...
}
}
- Автоматическое перенаправление запросов специфическими средствами языка.
Такими как, например, анонимная функция __call() в PHP, начиная с 5-ой версии, автоматически вызываемая средой языка если происходит попытка вызвать у объекта несуществующий в нем метод.
В Smalltalk для этих целей используется механизм doesNotUnderstand.
В любом случае мы получаем функционал, при котором сообщения, не имеющие соответствующих методов, перехватываются реализацией __call(), doesNotUnderstand, которая может быть замещена для перенаправления сообщения объекту-преемнику. Поэтому осуществлять перенаправление вручную уже не придется.
Класс обрабатывает только запросы, в которых заинтересован, и ожидает, что механизм doesNotUnderstand()/__call() выполнит обработку по-умолчанию для всех остальных запросов.
Результаты
Паттерн цепочка обязанностей имеет следующие достоинства и недостатки:
- Ослабление связанности.
Этот паттерн освобождает объект от необходимости «знать», кто конкретно обработает его запрос. Отправителю и получателю ничего неизвестно друг о друге, а включенному в цепочку объекту - о структуре цепочки.
Таким образом, цепочка обязанностей помогает упростить взаимосвязи между объектами. Вместо того чтобы хранить ссылки на все объекты, которые могут стать получателями запроса, объект должен располагать информацией лишь о своем ближайшем преемнике.
- Дополнительная гибкость при распределении обязанностей между объектами.
Цепочка обязанностей позволяет повысить гибкость распределения обязанностей между объектами. Добавить или изменить обязанности по обработке запроса можно, включив в цепочку новых участников или изменив ее каким-то другим образом. Этот подход можно сочетать со статическим порождением подклассов для создания специализированных обработчиков.
- Получение не гарантировано.
Поскольку у запроса нет явного получателя, то нет и гарантий, что он вообще будет обработан: он может достичь конца цепочки и пропасть. Необработанным запрос может оказаться и в случае неправильной конфигурации цепочки.
Пример
В языке программирования Java использование
Паттерн цепочка обязанностей самым непосредственным образом включено в ядро языка. Я, например, понял это в следующий момент, после того, когда подумал о каком-нибудь примере этого паттерна на java. Если еще не догадались, то речь идет о системе обработки исключений языка, т.е. exception-ов. Как известно, там имеется специальный тип исключений –
объявляемые исключения (checked exceptions). Кратко: если какой-то метод объявляет такое исключение, то любой другой метод, который его вызывает должен либо перехватить и обработать это исключение, либо объявить его в своей сигнатуре, таким образом, передавая эту обязанность вверх по цепочке вызовов. Т.е здесь имеем, что в качестве запроса выступает исключение (ошибка, исключительная ситуация времени выполнения), которое может выброситься любым участком кода во время выполнения программы. Выражаясь терминами паттерна, этим отсылается запрос на обработку этого исключения вверх по всей иерархии вызовов этого метода. Наиболее ответственный методов в этой цепочке вызовов и обрабатывает эту ошибку, прекращая ее следование вверх по иерархии, в противном случае если она не будет перехвачена – в конце концов вызывается специальный метод, по-умолчанию, печатающий стек-трейс ошибки в консоль.
Представим, что у нас имеется некоторый модуль отображения списка новостей.
У новости есть заголовок, текст и собственно дата новости: News.
Новости хранятся в базе данных и для того, чтобы их оттуда доставать любые модули, совершенно ничего не знающие ни о каких базах, определена сущность DAO (Data Access Object), у которой имеются 2 необходимых метода: достать одну последнюю новость или список всех новостей: DAO_I.
В зависимости от конкретной СУБД, реализация этого интерфейса может, конечно, отличаться (разный текст sql-запросов и т.д.), для нашей базы подходит следующий:
DAO.
Почти любое обращение к базе может сгенерировать объявляемое исключение SQLException, и мы, соответственно, можем либо тут же его перехватить и обработать, либо объявить в сигнатуре метода, передав эту ответственность вызвавшему методу верхнего уровня. В методе GetAllNews() выполняется PreparedStatement, выводящий список всех новостей. В случае ошибки выборки списка новостей система должна сразу уведомить об этом пользователя, поэтому класс DAO тут же обрабатывает этот запрос и выводит информацию об ошибке в консоль пользователя.
С методом GetLatestNews(), получающим самую свежую новость – ситуация обратная. Для нашей системы, например, главной функцией является все же функция вывода списка новостей за день, а не отображение самой последней новости, поэтому данный метод не должен таким же способом обрабатывать эту ошибку и мы делаем более гибкое решение, передавая ответственность за это вверх по цепочке (объявляя SQLException во throws этого метода).
Ну и главный класс нашего модуля это
NewsPage, связывающий все это воедино:
NewsPage.
Как видим метод Draw, отображающий всю информацию, принимает на себя ответственность за обработку исключения об ошибке выборки свежей новости – а именно просто делает запись об этом в логе. Обрабатывать исключение об ошибке выборке всех новостей – ему уже не нужно, поскольку это было сделано слоем ниже, в классе DAO.
В каркасе графических редакторов
Unidraw определены объекты
Command, которые инкапсулируют запросы к объектам
Component и
ComponentView. Объекты
Command - это запросы, которые компонент или вид компонента могут интерпретировать как команду на выполнение определенной операции.
Это соответствует подходу «запрос как объект», описанному в разделе «Реализация». Компоненты и виды компонентов могут быть организованы иерархически.
Как компонент, так и его вид могут перепоручать интерпретацию команды своему
родителю, тот - своему родителю и так далее, то есть речь идет о типичной цепочке обязанностей.
В
ЕТ++ паттерн цепочка обязанностей применяется для обработки запросов на обновление графического изображения. Графический объект вызывает операцию InvalidateRect всякий раз, когда возникает необходимость обновить часть занимаемой им области. Но выполнить эту операцию самостоятельно графический объект не может, так как не имеет достаточной информации о своем контексте, например из-за того, что окружен такими объектами, как Scroller (полоса прокрутки) или Zoomer (лупа), которые преобразуют его систему координат. Это означает, что объект может быть частично невидим, так как он оказался за границей области прокрутки или изменился его масштаб. Поэтому реализация InvalidateRect по умолчанию переадресует запрос контейнеру, где находится соответствующий объект. Последний объект в цепочке обязанностей — экземпляр класса Window. Гарантируется, что к тому моменту, как Window получит запрос, недействительный прямоугольник будет трансформирован правильно. Window
обрабатывает InvalidateRect, послав запрос интерфейсу оконной системы и требуя тем самым выполнить обновление.