Э. Гамма, Р. Хелм
Скачать 6.37 Mb.
|
ГЛАВА 5 ПАТТЕРНЫ ПОВЕДЕНИЯ Паттерны поведения связаны с алгоритмами и распределением обязанностей между объектами. Речь в них идет не только о самих объектах и классах, но и о типичных схемах взаимодействия между ними. Паттерны поведения характеризуют сложный поток управления, который трудно проследить во время выполнения программы. Внимание акцентируется не на схеме управления как таковой, а на связях между объектами. В паттернах поведения уровня класса для распределения поведения между разными классами используется наследование. В этой главе описано два таких паттерна. Из них более простым и широко распространенным яв- ляется шаблонный метод (373), который представляет собой абстрактное определение алгоритма. Алгоритм здесь определяется пошагово. На каждом шаге вызывается либо примитивная, либо абстрактная операция. Алгоритм детализируется за счет подклассов, где определяются абстрактные операции. Другой паттерн поведения уровня класса — интерпретатор (287) — представ- ляет грамматику языка в виде иерархии классов и реализует интерпретатор как последовательность операций над экземплярами этих классов. В паттернах поведения уровня объектов используется не наследование, а композиция. Некоторые из них описывают, как с помощью кооперации множество равноправных объектов справляется с задачей, которая ни одному из них не под силу. Важно здесь то, как объекты получают инфор- мацию о существовании друг друга. Одноранговые объекты могут хранить ссылки друг на друга, но это увеличит степень связанности системы. При максимальной степени связанности каждому объекту пришлось бы иметь информацию обо всех остальных. Эту проблему решает паттерн посредник (319). Посредник, находящийся между объектами-коллегами, обеспечивает косвенность ссылок, необходимую для разрыва лишних связей. Паттерн Chain of Responsibility (цепочка обязанностей) 263 Паттерн цепочка обязанностей (263) позволяет и дальше уменьшать степень связанности. Он дает возможность посылать запросы объекту не напрямую, а по цепочке «объектов-кандидатов». Запрос может выполнить любой «кан- дидат», если это допустимо в текущем состоянии выполнения программы. Число кандидатов заранее не определено, а подбирать участников можно во время выполнения. Паттерн наблюдатель (339) определяет и поддерживает зависимости между объектами. Классический пример наблюдателя встречается в схеме «мо- дель —представление — контроллер» языка Smalltalk, где все виды модели уведомляются о любых изменениях ее состояния. Прочие паттерны поведения связаны с инкапсуляцией поведения в объекте и делегированием ему запросов. Паттерн стратегия (362) инкапсулирует алгоритм объекта, упрощая его спецификацию и замену. Паттерн команда (275) инкапсулирует запрос в виде объекта, который можно передавать как параметр, хранить в списке истории или использовать как-то иначе. Паттерн состояние (352) инкапсулирует состояние объекта таким обра- зом, что при изменении состояния объект может изменять свое поведение. Паттерн посетитель (379) инкапсулирует поведение, которое в противном случае пришлось бы распределять между классами, а паттерн итератор (302) абстрагирует механизм доступа и обхода объектов из некоторого агрегата. ПАТТЕРН CHAIN OF RESPONSIBILITY (ЦЕПОЧКА ОБЯЗАННОСТЕЙ) Название и классификация паттерна Цепочка обязанностей — паттерн поведения объектов. Назначение Позволяет избежать привязки отправителя запроса к его получателю, предо- ставляя возможность обработать запрос нескольким объектам. Связывает объекты-получатели в цепочку и передает запрос по этой цепочке, пока он не будет обработан. Мотивация Рассмотрим контекстнозависимую оперативную справку в графическом интерфейсе: пользователь может получить дополнительную информацию по любой части интерфейса, просто щелкнув на ней мышью. Содержание 264 Глава 5. Паттерны поведения справки зависит от того, какая часть интерфейса была выбрана и в каком контексте. Например, справка по кнопке в диалоговом окне может отли- чаться от справки по аналогичной кнопке в главном окне приложения. Если для некоторой части интерфейса справки нет, то система должна показать информацию о ближайшем контексте, в котором она находится — например, о диалоговом окне в целом. Следовательно, естественно было бы организовать справочную информацию от более конкретных разделов к более общим. Кроме того, ясно, что запрос на получение справки обрабатывается одним из нескольких объектов пользова- тельского интерфейса, а каким именно — зависит от контекста и имеющейся в наличии информации. Проблема в том, что объект, инициирующий запрос (например, кнопка), не знает, какой объект в конечном итоге предоставит справку. Необходи- мо каким-то образом отделить кнопку — инициатор запроса от объектов, которые могут предоставить справочную информацию. Паттерн цепочка обязанностей показывает, как это может происходить. Идея паттерна заключается в том, чтобы разорвать связь между отправителями и получателями, дав возможность обработать запрос нескольким объектам. За- прос перемещается по цепочке объектов, пока не будет обработан одним из них. Частное Общее aPrintButton Обработчик anOKButton Обработчик aSaveDialog Обработчик aPrintDialog Обработчик anApplication Обработчик Первый объект в цепочке получает запрос и либо обрабатывает его сам, либо направляет следующему кандидату в цепочке, который действует точно так же. У объекта, отправившего запрос, отсутствует информация об обработчи- ке. Мы говорим, что у запроса есть анонимный получатель (implicit receiver). Допустим, пользователь запрашивает справку по кнопке Print (печать). Она находится в диалоговом окне PrintDialog , содержащем информацию об объ- Паттерн Chain of Responsibility (цепочка обязанностей) 265 екте приложения, которому принадлежит (см. предыдущую диаграмму). На следующей диаграмме взаимодействий показано, как запрос на получение справки перемещается по цепочке. aPrintDialog aPrintButton anApplication HandleHelp() HandleHelp() В данном случае ни кнопка aPrintButton , ни окно aPrintDialog не обраба- тывают запрос; он достигает объекта anApplication , который может его об- работать или игнорировать. У клиента, инициировавшего запрос, нет прямой ссылки на объект, который его в конце концов выполнит. Чтобы отправить запрос по цепочке и гарантировать анонимность полу- чателя, все объекты в цепочке имеют единый интерфейс для обработки за- просов и для доступа к своему преемнику (следующему объекту в цепочке). Например, в системе оперативной справки можно было бы определить класс HelpHandler (предок классов всех объектов-кандидатов или класс-примесь (mixin class)) с операцией HandleHelp . Тогда классы, которые хотят обраба- тывать запросы, могут сделать HelpHandler своим родителем: HelpHandler HandleHelp() Button HandleHelp() ShowHelp() Application Widget Dialog handler–>HandleHelp() if могу обработать { ShowHelp() } else { Handler::HandleHelp() } handler 266 Глава 5. Паттерны поведения Для обработки запросов на получение справки классы Button , Dialog и Application пользуются операциями HelpHandler . По умолчанию операция HandleHelp просто перенаправляет запрос своему преемнику. В подклассах эта операция замещается, так что при благоприятных обстоятельствах может выдаваться справочная информация. В противном случае запрос отправля- ется дальше посредством реализации по умолчанию. Применимость Основные условия для применения паттерна цепочка обязанностей: запрос может быть обработан более чем одним объектом, причем насто- ящий обработчик заранее неизвестен и должен быть найден автомати- чески; запрос должен быть отправлен одному из нескольких объектов, без яв- ного указания, какому именно; набор объектов, способных обработать запрос, должен задаваться дина- мически. Структура Handler HandleRequest() Client ConcreteHandler1 HandleRequest() ConcreteHandler2 HandleRequest() Преемник Типичная структура объектов выглядит примерно так: aClient aHandler aConcreteHandler Преемник aConcreteHandler Преемник Паттерн Chain of Responsibility (цепочка обязанностей) 267 Участники Handler ( HelpHandler ) — обработчик: • определяет интерфейс для обработки запросов; • (необязательно) реализует связь с преемником; ConcreteHandler ( PrintButton , PrintDialog ) — конкретный обработчик: • обрабатывает запрос, за который отвечает; • имеет доступ к своему преемнику; • если ConcreteHandler способен обработать запрос, то так и делает, если не может, то направляет его своему преемнику; Client — клиент: • отправляет запрос некоторому объекту ConcreteHandler в цепочке. Отношения Запрос, инициированный клиентом, продвигается по цепочке, пока неко- торый объект ConcreteHandler не возьмет на себя ответственность за его обработку. Результаты Основные достоинства и недостатки паттерна цепочка обязанностей: ослабление связанности. Паттерн освобождает объект от необходимости знать, кто конкретно обработает его запрос. Отправитель и получатель ничего не знают друг о друге, а включенный в цепочку объект — о струк- туре цепочки. В результате цепочка обязанностей помогает упростить взаимосвязи между объектами. Вместо хранения ссылок на все объекты, которые могут стать получателями запроса, объект должен располагать информацией лишь о своем ближайшем преемнике; дополнительная гибкость при распределении обязанностей между объ- ектами. Цепочка обязанностей позволяет повысить гибкость распределе- ния обязанностей между объектами. Добавить или изменить обязанно- сти по обработке запроса можно, включив в цепочку новых участников или изменив ее каким-то другим образом. Этот подход можно сочетать со статическим порождением подклассов для создания специализиро- ванных обработчиков; 268 Глава 5. Паттерны поведения получение не гарантировано. Поскольку у запроса нет явного получате- ля, то нет и гарантий, что он вообще будет обработан: он может достичь конца цепочки и пропасть. Необработанным запрос может оказаться и в случае неправильной конфигурации цепочки. Реализация При рассмотрении цепочки обязанностей следует обратить внимание на следующие аспекты: реализация цепочки преемников. Такую цепочку можно реализовать дву- мя способами: • определить новые связи (обычно это делается в классе Handler , но можно и в ConcreteHandler ); • использовать существующие связи. До сих пор в наших примерах определялись новые связи, однако можно воспользоваться уже имеющимися ссылками на объекты для формиро- вания цепочки преемников. Например, ссылка на родителя в иерархии «часть — целое» может заодно определять и преемника «части». В струк- туре виджетов такие связи тоже могут существовать. В разделе, посвя- щенном паттерну компоновщик (196), ссылки на родителей обсуждаются более подробно. Существующие связи хорошо работают тогда, когда они уже поддержива- ют нужную цепочку. Это позволит избежать явного определения новых связей и сэкономить память. Но если структура не отражает устройства цепочки обязанностей, то уйти от определения избыточных связей не удастся; соединение преемников. Если готовых ссылок, которые могли бы исполь- зоваться для определения цепочки, нет, то их придется ввести. В таком случае класс Handler не только определяет интерфейс запросов, но еще и хранит ссылку на преемника. Следовательно у обработчика появля- ется возможность определить реализацию операции HandleRequest по умолчанию — перенаправление запроса преемнику (если таковой существует). Если запрос не представляет интереса для подкласса ConcreteHandler , то последнему не нужно замещать эту операцию, по- скольку по умолчанию запрос будет отправлен дальше. Пример базового класса HelpHandler , в котором хранится указатель на преемника: Паттерн Chain of Responsibility (цепочка обязанностей) 269 class HelpHandler { public: HelpHandler(HelpHandler* s) : _successor(s) { } virtual void HandleHelp(); private: HelpHandler* _successor; }; void HelpHandler::HandleHelp () { if (_successor) { _successor->HandleHelp(); } } представление запросов. Представлять запросы можно по-разному. В простейшей форме (как в классе HandleHelp ) запрос жестко коди- руется в виде вызова некоторой операции. Это удобно и безопасно, но переадресовывать тогда можно только фиксированный набор запросов, определенных в классе Handler Альтернатива — использование одной функции-обработчика, которой передается код запроса (скажем, целое число или строка). Так можно поддержать заранее неизвестное число запросов. Единственное требова- ние состоит в том, что отправитель и получатель должны договориться о способе кодирования запроса. Этот подход более гибок, но при реализации нужно использовать услов- ные операторы для передачи запросов в зависимости от их кодов. Кроме того, не существует безопасного с точки зрения типов способа передачи параметров, поэтому упаковывать и распаковывать их приходится вруч- ную. Очевидно, что это не так безопасно, как прямой вызов операции. Чтобы решить проблему передачи параметров, допустимо использовать отдельные объекты-запросы, в которых инкапсулируются параметры запроса. Класс Request может представлять некоторые запросы явно, а их новые типы описываются в подклассах. Подкласс может определить другие параметры. Чтобы получить доступ к этим параметрам, обработчик должен располагать информацией о типе запроса (какой именно подкласс Request используется). Для идентификации запроса в классе Request можно определить функ- цию доступа, которая возвращает идентификатор класса. Вместо этого получатель мог бы воспользоваться информацией о типе, доступной во время выполнения, если язык программирования поддерживает такую возможность. 270 Глава 5. Паттерны поведения Приведем пример функции диспетчеризации, в которой используются объекты для идентификации запросов. Операция GetKind , указанная в базовом классе Request , определяет вид запроса: void Handler::HandleRequest (Request* theRequest) { switch (theRequest->GetKind()) { case Help: // привести аргумент к подходящему типу HandleHelp((HelpRequest*) theRequest); break; case Print: HandlePrint((PrintRequest*) theRequest); // ... break; default: // ... break; } } Подклассы могут расширить схему диспетчеризации, переопределив опе- рацию HandleRequest . Подкласс обрабатывает лишь те запросы, в которых заинтересован, а остальные отправляет родительскому классу. В этом слу- чае подкласс именно расширяет, а не замещает операцию HandleRequest Например, подкласс ExtendedHandler расширяет операцию HandleRequest , определенную в классе Handler , следующим образом: class ExtendedHandler : public Handler { public: virtual void HandleRequest(Request* theRequest); // ... }; void ExtendedHandler::HandleRequest(Request*theRequest){ switch (theRequest->GetKind()) { case Preview: // Обработать запрос Preview break; default: // Дать классу Handler возможность // обработать остальные запросы Handler::HandleRequest(theRequest); } } Паттерн Chain of Responsibility (цепочка обязанностей) 271 автоматическое перенаправление запросов в языке Smalltalk. С этой целью можно использовать механизм doesNotUnderstand . Сообщения, не имеющие соответствующих методов, перехватываются реализаци- ей doesNotUnderstand , которая может быть замещена для перенаправ- ления сообщения объекту-преемнику. Таким образом, осуществлять перенаправление вручную необязательно. Класс обрабатывает только те запросы, в которых он заинтересован, и рассчитывает, что механизм doesNotUnderstand перенаправит все остальные. Пример кода Следующий пример показывает, как с помощью цепочки обязанностей можно обработать запросы к описанной выше системе оперативной справки. Запрос на получение справки — это явная операция. Мы воспользуемся уже имеющимися в иерархии виджетов ссылками для перемещения запросов по цепочке от одного виджета к другому и определим в классе Handler от- дельную ссылку, чтобы можно было передать запрос включенным в цепочку объектам, не являющимся виджетами. Класс HelpHandler определяет интерфейс для обработки запросов на получе- ние справки. В нем хранится раздел справки (по умолчанию пустой) и ссыл- ка на преемника в цепочке обработчиков. Основной операцией является HandleHelp , которая замещается в подклассах. HasHelp — это вспомогательная операция, проверяющая, ассоциирован ли с объектом какой-нибудь раздел: typedef int Topic; const Topic NO_HELP_TOPIC = -1; class HelpHandler { public: HelpHandler(HelpHandler* = 0, Topic = NO_HELP_TOPIC); virtual bool HasHelp(); virtual void SetHandler(HelpHandler*, Topic); virtual void HandleHelp(); private: HelpHandler* _successor; Topic _topic; }; HelpHandler::HelpHandler ( HelpHandler* h, Topic t ) : _successor(h), _topic(t) { } bool HelpHandler::HasHelp () { return _topic != NO_HELP_TOPIC; } 272 Глава 5. Паттерны поведения void HelpHandler::HandleHelp () { if (_successor != 0) { _successor->HandleHelp(); } } Все виджеты — подклассы абстрактного класса Widget , который, в свою очередь, является подклассом HelpHandler , так как со всеми элементами пользовательского интерфейса может быть ассоциирована справочная информация. (Конечно, можно было построить реализацию и на основе класса-примеси.) class Widget : public HelpHandler { protected: Widget(Widget* parent, Topic t = NO_HELP_TOPIC); private: Widget* _parent; }; Widget::Widget (Widget* w, Topic t) : HelpHandler(w, t) { _parent = w; } В нашем примере первым обработчиком в цепочке является кнопка. Класс Button — это подкласс Widget . Конструктор класса Button получает два параметра — ссылку на виджет, в котором он находится, и раздел справки: class Button : public Widget { public: Button(Widget* d, Topic t = NO_HELP_TOPIC); virtual void HandleHelp(); // Операции класса Widget, которые Button замещает... }; Реализация HandleHelp из класса Button сначала проверяет, есть ли для кноп- ки справочная информация. Если разработчик не определил ее, то запрос от- правляется преемнику с помощью операции HandleHelp класса HelpHandler Если же информация есть, то кнопка ее выводит, и поиск заканчивается: Button::Button (Widget* h, Topic t) : Widget(h, t) { } void Button::HandleHelp () { if (HasHelp()) { // Предоставить справку по кнопке } else { HelpHandler::HandleHelp(); } } |