Э. Гамма, Р. Хелм
Скачать 6.37 Mb.
|
273 Класс Dialog реализует аналогичную схему, только его преемником явля- ется не виджет, а произвольный обработчик запроса на справку. В нашем приложении таким преемником выступает экземпляр класса Application : class Dialog : public Widget { public: Dialog(HelpHandler* h, Topic t = NO_HELP_TOPIC); virtual void HandleHelp(); // Операции класса Widget, которые Dialog замещает... // ... }; Dialog::Dialog (HelpHandler* h, Topic t) : Widget(0) { SetHandler(h, t); } void Dialog::HandleHelp () { if (HasHelp()) { // Предоставить справку по диалоговому окну } else { HelpHandler::HandleHelp(); } } В конце цепочки находится экземпляр класса Application . Приложение — это не виджет, поэтому Application — прямой потомок класса HelpHandler . Если запрос на получение справки дойдет до этого уровня, то класс Application может выдать информацию о приложении в целом или предложить список разделов справки: class Application : public HelpHandler { public: Application(Topic t) : HelpHandler(0, t) { } virtual void HandleHelp(); // Операции, относящиеся к самому приложению... }; void Application::HandleHelp () { // Показать список разделов справки } Следующий код создает и связывает эти объекты. В данном случае рас- сматривается диалоговое окно Print, поэтому с объектами связаны разделы справки, относящиеся к печати: const Topic PRINT_TOPIC = 1; const Topic PAPER_ORIENTATION_TOPIC = 2; const Topic APPLICATION_TOPIC = 3; 274 Глава 5. Паттерны поведения Application* application = new Application(APPLICATION_TOPIC); Dialog* dialog = new Dialog(application, PRINT_TOPIC); Button* button = new Button(dialog, PAPER_ORIENTATION_TOPIC); Мы можем инициировать запрос на получение справки, вызвав операцию HandleHelp для любого объекта в цепочке. Чтобы начать поиск с объекта кнопки, достаточно выполнить его операцию HandleHelp : button->HandleHelp(); В этом примере кнопка обрабатывает запрос сразу же. Заметим, что класс HelpHandler можно было бы сделать преемником Dialog . Более того, его преемника можно изменять динамически. Вот почему, где бы диалоговое окно ни встретилось, вы всегда получите справочную информацию с учетом контекста. Известные применения Паттерн цепочка обязанностей используется в нескольких библиотеках классов для обработки событий, инициированных пользователем. Класс Handler в них называется по-разному, но идея всегда одна и та же: когда пользователь щелкает кнопкой мыши или нажимает клавишу, генерируется некоторое событие, которое распространяется по цепочке. В MacApp [App89] и ET++ [WGM88] класс называется EventHandler , в библиотеке TCL фирмы Symantec [Sym93b] — Bureaucrat , а в библиотеке из системы NeXT [Add94] используется имя Responder В каркасе графических редакторов Unidraw определены объекты Command , которые инкапсулируют запросы к объектам Component и ComponentView [VL90]. Объекты Command являются запросами в том смысле, что они могут интерпретироваться компонентом или представлением компонента как команда на выполнение определенной операции. Это соответствует подхо- ду «запрос как объект», описанному в разделе «Реализация». Компоненты и виды компонентов могут быть организованы иерархически. Как компонент, так и его представление могут перепоручать интерпретацию команды своему родителю, тот — своему родителю и так далее, то есть речь идет о типичной цепочке обязанностей. В ET++ паттерн цепочка обязанностей применяется для обработки запросов на обновление графического изображения. Графический объект вызывает операцию InvalidateRect всякий раз, когда возникает необходимость об- новить часть занимаемой им области. Но выполнить эту операцию само- стоятельно графический объект не может, так как не имеет достаточной Паттерн Command (команда) 275 информации о своем контексте, например из-за того, что окружен такими объектами, как Scroller (полоса прокрутки) или Zoomer (лупа), которые преобразуют его систему координат. Это означает, что объект может быть частично невидим, так как он оказался за границей области прокрутки или изменился его масштаб. Поэтому реализация InvalidateRect по умолчанию переадресует запрос контейнеру, где находится соответствующий объект. Последний объект в цепочке обязанностей — экземпляр класса Window К тому моменту, когда Window получит запрос, недействительный прямо- угольник будет гарантированно преобразован правильно. Window обрабаты- вает InvalidateRect , послав запрос интерфейсу оконной системы и требуя тем самым выполнить обновление. Родственные паттерны Паттерн цепочка обязанностей часто применяется вместе с паттерном компо- новщик (196). В этом случае родитель компонента может выступать в роли его преемника. ПАТТЕРН COMMAND (КОМАНДА) Название и классификация паттерна Команда — паттерн поведения объектов. Назначение Инкапсулирует запрос в объекте, позволяя тем самым параметризовать кли- енты для разных запросов, ставить запросы в очередь или протоколировать их, а также поддерживать отмену операций. Другие названия Action (действие), Transaction (транзакция). Мотивация Иногда необходимо посылать объектам запросы, ничего не зная о том, вы- полнение какой операции запрошено и кто является получателем. Например, в библиотеках для построения пользовательских интерфейсов встречаются такие объекты, как кнопки и меню, которые посылают запрос в ответ на действие пользователя. Но сама библиотека не может явно реализовать запрос в кнопке или меню, потому что только приложение, использующее 276 Глава 5. Паттерны поведения библиотеку, располагает информацией о том, что следует сделать. Проек- тировщик библиотеки ничего не знает о получателе запроса и о том, какие операции тот должен выполнить. Паттерн команда позволяет библиотечным объектам отправлять запросы неизвестным объектам приложения, преобразовав сам запрос в объект. Этот объект можно хранить и передавать, как и любой другой. В основе списываемого паттерна лежит абстрактный класс Command , в котором объ- явлен интерфейс для выполнения операций. В простейшей своей форме этот интерфейс состоит из одной абстрактной операции Execute. Конкретные подклассы Command определяют пару «получатель — действие», сохраняя получателя в переменной экземпляра, и реализуют операцию Execute , так чтобы она посылала запрос. У получателя есть информация, необходимая для выполнения запроса. Document Open() Close() Cut() Copy() Paste() Menu Add(MenuItem) MenuItem Clicked() Command Execute() command–>Execute() Application Add(Document) command Меню легко реализуются с помощью объектов Command . Каждый пункт меню — это экземпляр класса MenuItem . Сами меню и все их пункты создает класс Application наряду со всеми остальными элементами пользователь- ского интерфейса. Класс Application отслеживает также открытые пользо- вателем документы. Приложение конфигурирует каждый объект MenuItem экземпляром кон- кретного подкласса Command . Когда пользователь выбирает некоторый пункт меню, ассоциированный с ним объект MenuItem вызывает Execute для своего объекта-команды, а Execute выполняет операцию. Объекты MenuItem не имеют информации, какой подкласс класса Command они используют. Под- классы Command хранят информацию о получателе запроса и вызывают одну или несколько операций этого получателя. Например, подкласс PasteCommand поддерживает вставку текста из буфера обмена в документ. Получателем для PasteCommand является объект Document , который был передан при создании объекта. Операция Execute вызывает операцию Paste документа-получателя. Паттерн Command (команда) 277 document–>Paste() Command Execute() PasteCommand Execute() Document Open() Close() Cut() Copy() Paste() Документ Для подкласса OpenCommand операция Execute ведет себя по-другому: она за- прашивает у пользователя имя документа, создает соответствующий объект Document , оповещает о новом документе приложение-получатель и открывает этот документ. Command Execute() Application Add(Document) OpenCommand Execute() AskUser() name=AskUser() doc=newDocument(name) application –>Add(doc) doc –>Open() Приложения Иногда объект MenuItem должен выполнить последовательность команд. На- пример, пункт меню для центрирования страницы стандартного размера мож- но было бы сконструировать сразу из двух объектов: CenterDocumentCommand и NormalSizeCommand . Поскольку такое комбинирование команд — явление обычное, то мы можем определить класс MacroCommand , позволяющий объ- екту MenuItem выполнять произвольное число команд. MacroCommand — это конкретный подкласс класса Command , который просто выполняет последо- вательность команд. У него нет явного получателя, поскольку для каждой команды определен свой собственный. 278 Глава 5. Паттерны поведения Command Execute() MacroCommand Execute() Для всех команд с c–>Execute() Команды Обратите внимание: в каждом из этих примеров паттерн команда отделяет объект, инициирующий операцию, от объекта, который располагает инфор- мацией, необходимой для ее выполнения. Это позволяет добиться высокой гибкости при проектировании пользовательского интерфейса. Пункт меню и кнопка одновременно могут быть ассоциированы в приложении с неко- торой функцией, для этого достаточно приписать обоим элементам один и тот же экземпляр конкретного подкласса класса Command . Команды могут заменяться динамически, что очень полезно для реализации контекстно- зависимых меню. Можно также поддержать сценарии, если компоновать простые команды в более сложные. Все это выполнимо потому, что объект, инициирующий запрос, должен располагать информацией лишь о том, как его отправить, а не о том, как он должен выполняться. Применимость Основные условия для применения паттерна команда: параметризация объектов выполняемым действием, как в случае с пун- ктами меню MenuItem. В процедурном языке такую параметризацию можно выразить с помощью функции обратного вызова, то есть такой функции, которая регистрируется, чтобы быть вызванной позднее. Ко- манды представляют собой объектно-ориентированную альтернативу функциям обратного вызова; определение, постановка в очередь и выполнение запросов в разное время. Время жизни объекта Command не обязательно должно зависеть от времени жизни исходного запроса. Если получатель запроса удается реализовать так, чтобы он не зависел от адресного пространства, то объект-команду можно передать другому процессу, который займется его выполнением; поддержка отмены операций. Операция Execute объекта Command может сохранить состояние, необходимое для отмены действий, выполненных Паттерн Command (команда) 279 командой. В этом случае в интерфейсе класса Command должна быть до- полнительная операция Unexecute , которая отменяет действия, выпол- ненные предшествующим обращением к Execute . Выполненные коман- ды хранятся в списке истории. Для реализации произвольного числа уровней отмены и повтора команд нужно обходить этот список соответ- ственно в обратном и прямом направлениях, вызывая при посещении каждого элемента команду Unexecute или Execute ; поддержка протоколирования изменений, чтобы их можно было вы- полнить повторно после сбоя в системе. Дополнив интерфейс класса Command операциями сохранения и загрузки, вы сможете вести протокол изменений во внешней памяти. Для восстановления после сбоя нужно будет загрузить сохраненные команды с диска и повторно выполнить их с помощью операции Execute ; структурирование системы на основе высокоуровневых операций, по- строенных из примитивных. Такая структура типична для информа- ционных систем с поддержкой транзакций. Транзакция инкапсулиру- ет набор изменений данных. Паттерн команда позволяет моделировать транзакции. У всех команд есть общий интерфейс, что дает возможность работать одинаково с любыми транзакциями. С помощью этого паттер- на можно легко добавлять в систему новые виды транзакций. Структура Command Execute() Receiver Invoker Client Action() ConcreteCommand Execute() Состояние receiver–>Action(); receiver Участники Command — команда: • объявляет интерфейс для выполнения операции; ConcreteCommand ( PasteCommand , OpenCommand ) — конкретная команда: • определяет связь между объектом-получателем Receiver и действием; • реализует операцию Execute путем вызова соответствующих операций объекта Receiver ; 280 Глава 5. Паттерны поведения Client ( Application ) — клиент: • создает объект класса ConcreteCommand и устанавливает его получателя; Invoker ( MenuItem ) — инициатор: • обращается к команде для выполнения запроса; Receiver ( Document , Application ) — получатель: • располагает информацией о способах выполнения операций, не- обходимых для удовлетворения запроса. В роли получателя может выступать любой класс. Отношения клиент создает объект ConcreteCommand и устанавливает для него полу- чателя; инициатор Invoker сохраняет объект ConcreteCommand ; инициатор отправляет запрос, вызывая операцию команды Execute . Если поддерживается отмена выполненных действий, то ConcreteCommand пе- ред вызовом Execute сохраняет информацию о состоянии, достаточную для выполнения отмены; объект ConcreteCommand вызывает операции получателя для выполне- ния запроса. На следующей схеме видно, как Command разрывает связь между иници- атором и получателем (а также запросом, который должен выполнить последний). aClient aReceiver aCommand anInvoker Execute() Action() new Command(aReceiver) StoreCommand(aCommand) Паттерн Command (команда) 281 Результаты Основные результаты применения паттерна команда: команда отделяет объект, инициирующий операцию, от объекта, распо- лагающего информацией о том, как ее выполнить; команды — это самые настоящие объекты. Их можно обрабатывать и рас- ширять точно так же, как любые другие объекты; из простых команд можно собирать составные, например класс MacroCommand , рассмотренный выше. В общем случае составные коман- ды описываются паттерном компоновщик (196); новые команды добавляются легко, поскольку никакие существующие классы изменять не нужно. Реализация При реализации паттерна команда следует обратить внимание на следующие аспекты: насколько «умной» должна быть команда. У команды может быть ши- рокий круг обязанностей, от простого определения связи между полу- чателем и действиями, которые нужно выполнить для удовлетворения запроса, до самостоятельной реализации без обращения за помощью к получателю. Последний вариант полезен, когда вы хотите определить команды, не зависящие от существующих классов, когда подходящего получателя не существует или когда получатель команде точно не изве- стен. Например, команда, создающая новое окно приложения, может не понимать, что именно она создает, а трактовать окно как любой другой объект. Где-то посередине между двумя крайностями находятся коман- ды, обладающие достаточной информацией для динамического обнару- жения своего получателя; поддержка отмены и повтора операций. Команды могут поддерживать отмену и повтор операций, если имеется возможность отменить резуль- таты выполнения (например, операции Unexecute или Undo ). В классе ConcreteCommand может сохраняться необходимая для этого дополни- тельная информация, в том числе: • объект-получатель Receiver , который выполняет операции в ответ на запрос; • аргументы операции, выполненной получателем; 282 Глава 5. Паттерны поведения • исходные значения различных атрибутов получателя, которые могли из- мениться в результате обработки запроса. Получатель должен предоста- вить операции, позволяющие команде вернуться в исходное состояние. Для поддержки всего одного уровня отмены приложению достаточно сохранять только последнюю выполненную команду. Если же нужны многоуровневые отмена и повтор операций, то придется вести список истории выполненных команд. Максимальная длина этого списка и опре- деляет число уровней отмены и повтора. Проход по списку в обратном направлении и отмена результатов всех встретившихся по пути команд отменяет их действие; проход в прямом направлении и выполнение встретившихся команд приводит к повтору действий. Возможно, команду, допускающую отмену, придется скопировать перед помещением в список истории. Дело в том, что объект команды, ис- пользованный для доставки запроса, скажем от пункта меню MenuItem , позже мог быть использован для других запросов. Поэтому копирование необходимо, чтобы определить разные вызовы одной и той же команды, если ее состояние при любом вызове может изменяться. Например, ко- манда DeleteCommand , которая удаляет выбранные объекты, при каждом вызове должна сохранять разные наборы объектов. Следовательно, объект DeleteCommand необходимо скопировать после выполнения, а копию поме- стить в список истории. Если в результате выполнения состояние команды никогда не изменяется, то копировать не нужно — в список достаточно поместить лишь ссылку на команду. Команды, которые обязательно нуж- но копировать перед помещением в список истории, ведут себя подобно прототипам (см. описание паттерна прототип (146)); предотвращение накопления ошибок в процессе отмены. При обеспечении надежного, сохраняющего семантику механизма отмены и повтора может возникнуть проблема гистерезиса. При выполнении, отмене и повторе ко- манд иногда накапливаются ошибки, в результате чего состояние прило- жения оказывается отличным от первоначального. Поэтому порой при- ходится сохранять в команде больше информации, дабы гарантировать, что объекты будут целиком восстановлены. Чтобы предоставить команде доступ к этой информации, не раскрывая внутреннего устройства объек- тов, можно воспользоваться паттерном хранитель (330); применение шаблонов в C++. Для команд, которые: (1) не допускают от- мену и (2) не требуют аргументов при вызове, в языке C++ можно вос- пользоваться шаблонами, чтобы не создавать подкласс класса Command для каждой пары «действие — получатель». Как это сделать, мы проде- монстрируем в разделе «Пример кода». |