Э. Гамма Р. Хелм Р. Джонсон Дж. Влиссидес
Вид материала | Документы |
- Прослушивание цикла лекций; проведение лабораторных занятий по интерпретации результатов, 23.31kb.
- Космическое рентгеновское и гамма-излучение, 1234.69kb.
- Название эксперимента, 62.39kb.
- Оздоровительный комплекс «Гамма» 10 Отель «Гамма» 11 Пансионат «Светлана» 12 Экскурсия, 2786.29kb.
- Французский реечный потолок реечные потолки, 207.48kb.
- План выставки при IV международной конференции «металлургия-интехэко-2011» холл конференц-зала, 60.11kb.
- Исследование cnd- вещества, методом отражения рентгеновского и гамма – излучения, 75.73kb.
- Эффект Мёссбауэра 2ч, 233.13kb.
- Список художественной литературы для фс-3, фж-3, 15.57kb.
- Поэзия Марины Цветаевой Лакофф Дж., Джонсон М. Метафоры, которыми мы живем литература, 21.08kb.
Паттерны поведения много, то такое распределение эффективнее, так как в противном случае пришлось бы иметь дело с громоздкими условными операторами. Наличие громоздких условных операторов нежелательно, равно как и наличие длинных процедур. Они слишком монолитны, вот почему модификация и расширение кода становится проблемой. Паттерн состояние предлагает более удачный способ структурирования зависящего от состояния кода. Логика, описывающая переходы между состояниями, больше не заключена в монолитные операторы i f или switch, а распределена между подклассами State. При инкапсуляции каждого перехода и действия ъ класс состояние становится полноценным объектом. Это улучшает структуру кода и проясняет его назначение; Q делает явными переходы между состояниями. Если объект определяет свое текущее состояние исключительно в терминах внутренних данных, то переходы между состояниями не имеют явного представления; они проявляются лишь как присваивания некоторым переменным. Ввод отдельных объектов для различных состояний делает переходы более явными. Кроме того, объекты State могут защитить контекст Context от рассогласования внутренних переменных, поскольку переходы с точки зрения контекста - это атомарные действия. Для осуществления перехода надо изменить значение только одной переменной (объектной переменной State в классе Context), а не нескольких [dCLF93]; Q объекты состояния можно разделять. Если в объекте состояния State отсутствуют переменные экземпляра, то есть представляемое им состояние кодируется исключительно самим типом, то разные контексты могут разделять один и тот же объект State. Когда состояния разделяются таким образом, они являются, по сути дела, приспособленцами (см. описание паттерна приспособленец), у которых нет внутреннего состояния, а есть только поведение. Реализация С паттерном состояние связан целый ряд вопросов реализации: Q что определяет переходы между состояниями. Паттерн состояние ничего не сообщает о том, какой участник определяет критерий перехода между состояниями. Если критерии зафиксированы, то их можно реализовать непосредственно в классе Context. Однако в общем случае более гибкий и правильный подход заключается в том, чтобы позволить самим подклассам класса State определять следующее состояние и момент перехода. Для этого в класс Context надо добавить интерфейс, позволяющий объектам State установить состояние контекста. Такую децентрализованную логику переходов проще модифицировать и расширять - нужно лишь определить новые подклассы State. Недостаток децентрализации в том, что каждый подкласс State должен «знать» еще хотя бы об одном подклассе, что вносит реализационные зависимости между подклассами; Паттерн State Q табличная альтернатива. Том Каргилл (Tom Cargill) в книге C++Programming Style [Car92] описывает другой способ структурирования кода, управляемого сменой состояний. Он использует таблицу для отображения входных данных на переходы между состояниями. С ее помощью можно определить, в какое состояние нужно перейти при поступлении некоторых входных данных. По существу, тем самым мы заменяем условный код (или виртуальные функции, если речь идет о паттерне состояние) поиском в таблице. Основное преимущество таблиц - в их регулярности: для изменения критериев перехода достаточно модифицировать только данные, а не код. Но есть и недостатки:
Главное различие между конечными автоматами на базе таблиц и паттерном состояние можно сформулировать так: паттерн состояние моделирует поведение, зависящее от состояния, а табличный метод акцентирует внимание на определении переходов между состояниями; Q создание и уничтожение объектов состояния. В процессе разработки обычно приходится выбирать между:
Первый вариант предпочтителен, когда заранее неизвестно, в какие состояния будет попадать система, и контекст изменяет состояние сравнительно редко. При этом мы не создаем объектов, которые никогда не будут использованы, что существенно, если в объектах состояния хранится много информации. Когда изменения состояния происходят часто, поэтому не хотелось бы уничтожать представляющие их объекты (ибо они могут очень скоро понадобиться вновь), следует воспользоваться вторым подходом. Время на создание объектов затрачивается только один раз, в самом начале, а на уничтожение - не затрачивается вовсе. Правда, этот подход может оказаться неудобным, так как в контексте должны храниться ссылки на все состояния, в которые система теоретически может попасть; Q использование динамического наследования. Варьировать поведение по запросу можно, меняя класс объекта во время выполнения, но в большинстве объектно-ориентированных языков это не поддерживается. Исключение составляет Self [US87] и другие основанные на делегировании языки, которые предоставляют такой механизм и, следовательно, поддерживают паттерн состояние напрямую. Объекты в языке Self могут делегировать операции Паттерны поведения другим объектам, обеспечивая тем самым некую форму динамического наследования . С изменением целевого объекта делегирования во время выполнения, по существу, изменяется и структура графа наследования. Такой механизм позволяет объектам варьировать поведение путем изменения своего класса. Пример кода В следующем примере приведен код на языке C++ с TCP-соединением из раздела «Мотивация». Это упрощенный вариант протокола TCP, в нем, конечно же, представлен не весь протокол и даже не все состояния TCP-соединений.1 Прежде всего определим класс TCPConnection, который предоставляет интерфейс для передачи данных и обрабатывает запросы на изменение состояния: class TCPOctetStream; class TCPState; class TCPConnection { public: TCPConnection ( ) ; void ActiveOpen ( ) ; void PassiveOpenO ; void Close ( ) ; void Send() ; void Acknowledge (); void Synchronize (); void ProcessOctet (TCPOctetStream*) ; private: friend class TCPState; void ChangeState (TCPState*); private: TCPState* _state; }; В переменной-члене _state класса TCPConnection хранится экземпляр класса TCPState. Этот класс дублирует интерфейс изменения состояния, определенный в классе TCPConnect ion. Каждая операция TCPState принимает экземпляр TCPConnection как параметр, тем самым позволяя объекту TCPState получить доступ к данным объекта TCPConnection и изменить состояние соединения: class TCPState { public: virtual void Transmit(TCPConnection*, TCPOctetStream*); virtual void ActiveOpen(TCPConnection*); virtual void PassiveOpen(TCPConnection*); virtual void Close(TCPConnection*); 1 Пример основан на описании протокола установления TCP-соединений, приведенном в книге Линча и Роуза [LR93]. % Паттерн State virtual void Synchronize (TCPConnection*) ; virtual void Acknowledge (TCPConnection*) ; virtual void Send (TCPConnect ion* ) ; protected: void ChangeState (TCPConnection*, TCPState*); TCPConnection делегирует все зависящие от состояния запросы хранимому в _state экземпляру TCPState. Кроме того, в классе TCPConnection существует операция, с помощью которой в эту переменную можно записать указатель на другой объект TCPState. Конструктор класса TCPConnection инициализирует _state указателем на состояние TCPClosed (мы определим его ниже): TCPConnection: : TCPConnection () { _state = TCPClosed: : Instance () ; void TCPConnection::ChangeState (TCPState* s) { _state = s; void TCPConnection::ActiveOpen () { _state->ActiveOpen(this); void TCPConnection::PassiveOpen () { _state->PassiveOpen(this); oid TCPConnection::Close () { _state->Close(this); void TCPConnection:Acknowledge () { _state->Acknowledge (this) ; void TCPConnection::Synchronize () { _state->Synchronize(this); } В классе TCPState реализовано поведение по умолчанию для всех делегированных ему запросов. Он может также изменить состояние объекта TCPConnection посредством операции ChangeState. TCPState объявляется другом класса TCPConnection, что дает ему привилегированный доступ к этой операции: void TCPState::Transmit (TCPConnection*, TCPOctetStream*) { } void TCPState::ActiveOpen (TCPConnection*) { } void TCPState::PassiveOpen (TCPConnection*) { } Паттерны поведения void TCPState: : Close (TCPConnection*) { } void TCPState: : Synchronize (TCPConnection*) { } void TCPState: :ChangeState (TCPConnection* t, TCPState* s) { t->ChangeState(s) ; } В подклассах TCPState реализовано поведение, зависящее от состояния. Соединение TCP может находиться во многих состояниях: Established (установлено), Listening (прослушивание), Closed (закрыто) и т.д., и для каждого из них есть свой подкласс TCPState. Мы подробно рассмотрим три подкласса -TCPEstablished, TCPListen и TCPClosed: class TCPEstablished : public TCPState { public: static TCPState* Instanced; virtual void Transmit (TCPConnection*, TCPOctetStream*) ; virtual void Close (TCPConnection*) ; ) / class TCPListen : public TCPState { public: static TCPState* Instance(); virtual void Send(TCPConnection*) ; class TCPClosed : public TCPState { public: static TCPState* Instanced; virtual void ActiveOpen(TCPConnection*); virtual void PassiveOpen(TCPConnection*); В подклассах TCPState нет никакого локального состояния, поэтому их можно разделять, так что потребуется только по одному экземпляру каждого класса. Уникальный экземпляр подкласса TCPState создается обращением к статической операции Instance.1 В подклассах TCPState реализовано зависящее от состояния поведение для тех запросов, которые допустимы в этом состоянии: void TCPClosed::ActiveOpen (TCPConnection* t) { // послать SYN, получить SYN, ACK и т.д. ChangeState(t, TCPEstablished::Instanced); Таким образом, каждый подкласс TCPState - это одиночка. Паттерн State void TCPClosed::PassiveOpen (TCPConnection* t) { ChangeState(t, TCPListen::Instance))); void TCPEstablished::Close (TCPConnection* t) { // послать FIN, получить АСК для FIN ChangeStateft, TCPListen::Instance))) ; void TCPEstablished::Transmit ( TCPConnection* t, TCPOctetStream* о ) { t->ProcessOctet(o); void TCPListen::Send (TCPConnection* t) { // послать SYN, получить SYN, АСК и т.д. ChangeStateft, TCPEstablished::Instanced); } После выполнения специфичных для своего состояния действий эти операции вызывают ChangeState для изменения состояния объекта TCPConnection. У него нет никакой информации о протоколе TCP. Именно подклассы TCPState определяют переходы между состояниями и действия, диктуемые протоколом. Известные применения Ральф Джонсон и Джонатан Цвейг [JZ91] характеризуют паттерн состояние и описывают его применительно к протоколу TCP. Наиболее популярные интерактивные программы рисования предоставляют «инструменты» для выполнения операций прямым манипулированием. Например, инструмент для рисования линий позволяет пользователю щелкнуть в произвольной точке мышью, а затем, перемещая мышь, провести из этой точки линию. Инструмент для выбора позволяет выбирать некоторые фигуры. Обычно все имеющиеся инструменты размещаются в палитре. Работа пользователя заключается в том, чтобы выбрать и применить инструмент, но на самом деле поведение редактора варьируется при смене инструмента: посредством инструмента для рисования мы создаем фигуры, при помощи инструмента выбора - выбираем их и т.д. Чтобы отразить зависимость поведения редактора от текущего инструмента, можно воспользоваться паттерном состояние. Можно определить абстрактный класс Tool, подклассы которого реализуют зависящее от инструмента поведение. Графический редактор хранит ссылку на текущий объект Tool и делегирует ему поступающие запросы. При выборе инструмента редактор использует другой объект, что приводит к изменению поведения. Данная техника используется в каркасах графических редакторов HotDraw [Joh92] и Unidraw [VL90]. Она позволяет клиентам легко определять новые виды Паттерны поведения инструментов. В HotDraw класс DrawingController переадресует запросы текущему объекту Tool. В Unidraw соответствующие классы называются Viewer и Tool. На приведенной ниже диаграмме классов схематично представлены интерфейсы классов Tool и DrawingController. Описанный Джеймсом Коплиеном [Сор92] прием конверт-письмо (Envelope-Letter) также относится к паттерну состояние. Техника конверт-письмо - это способ изменить класс объекта во время выполнения. Паттерн состояние является частным случаем, в нем акцент делается на работу с объектами, поведение которых зависит от состояния. Родственные паттерны Паттерн приспособленец подсказывает, как и когда можно разделять объекты класса State. Объекты класса state часто бывают одиночками. Паттерн Strategy Название и классификация паттерна Стратегия - паттерн поведения объектов. Назначение Определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Стратегия позволяет изменять алгоритмы независимо от клиентов, которые ими пользуются. Известен также под именем Policy (политика). Мотивация Существует много алгоритмов для разбиения текста на строки. Жестко «зашивать» все подобные алгоритмы в классы, которые в лих нуждаются, нежелательно по нескольким причинам: Паттерн Strategy Q клиент, которому требуется алгоритм разбиения на строки, усложняется при включении в него соответствующего кода. Таким образом, клиенты становятся более громоздкими, а сопровождать их труднее, особенно если нужно поддержать сразу несколько алгоритмов; Q в зависимости от обстоятельств стоит применять тот или иной алгоритм. Не хотелось бы поддерживать несколько алгоритмов разбиения на строки, если мы не будем ими пользоваться; О если разбиение на строки - неотъемлемая часть клиента, то задача добавления новых и модификации существующих алгоритмов усложняется. Всех этих проблем можно избежать, если определить классы, инкапсулирующие различные алгоритмы разбиения на строки. Инкапсулированный таким образом алгоритм называется стратегией. Предположим, что класс Composition отвечает за разбиение на строки текста, отображаемого в окне программы просмотра, и его своевременное обновление. Стратегии разбиения на строки определяются не в классе Composition, а в подклассах абстрактного класса Compositor. Это могут быть, например, такие стратегии: Q SimpleCompositor реализует простую стратегию, выделяющую по одной строке за раз; Q TeXCompositor реализует алгоритм поиска точек разбиения на строки, принятый в редакторе TJX. Эта стратегия пытается выполнить глобальную оптимизацию разбиения на строки, рассматривая сразу целый параграф; Q ArrayCompositor реализует стратегию расстановки переходов на новую строку таким образом, что в каждой строке оказывается одно и то же число элементов. Это полезно, например, при построчном отображении набора пиктограмм. Объект Composition хранит ссылку на объект Compositor. Всякий раз, когда объекту Composition требуется переформатировать текст, он делегирует данную обязанность своему объекту Compositor. Клиент указывает, какой объект Compositor следует использовать, параметризуя им объект Composition. Применимость Используйте паттерн стратегия, когда: Q имеется много родственных классов, отличающихся только поведением. Стратегия позволяет сконфигурировать класс, задав одно из возможных поведений; Паттерны поведения Q вам нужно иметь несколько разных вариантов алгоритма. Например, можно определить два варианта алгоритма, один из которых требует больше времени, а другой - больше памяти. Стратегии разрешается применять, когда варианты алгоритмов реализованы в виде иерархии классов [НО87]; О в алгоритме содержатся данные, о которых клиент не должен «знать». Используйте паттерн стратегия, чтобы не раскрывать сложные, специфичные для алгоритма структуры данных; Q в классе определено много поведений, что представлено разветвленными условными операторами. В этом случае проще перенести код из ветвей в отдельные классы стратегий. Структура Участники Q Strategy (Compositor) - стратегия: - объявляет общий для всех поддерживаемых алгоритмов интерфейс. Класс Context пользуется этим интерфейсом для вызова конкретного алгорит ма, определенного в классе ConcreteStrategy; Q ConcreteStrategy (SimpleCompositor, TeXCompositor, ArrayCompositor) - конкретная стратегия: - реализует алгоритм, использующий интерфейс, объявленный в классе Strategy; Q Context (Composition) - контекст:
Отношения Q классы Strategy и Context взаимодействуют для реализации выбранного алгоритма. Контекст может передать стратегии все необходимые алгоритму данные в момент его вызова. Вместо этого контекст может позволить обращаться к своим операциям в нужные моменты, передав ссылку на самого себя операциям класса Strategy; Q контекст переадресует запросы своих клиентов объекту-стратегии. Обычно клиент создает объект ConcreteStrategy и передает его контексту, после Паттерн Strategy чего клиент «общается» исключительно с контекстом. Часто в распоряжении клиента находится несколько классов ConcreteStrategy, которые он может выбирать. Результаты У паттерна стратегия есть следующие достоинства и недостатки: О семейства родственных алгоритмов. Иерархия классов Strategy определяет семейство алгоритмов или поведений, которые можно повторно использовать в разных контекстах. Наследование позволяет вычленить общую для всех алгоритмов функциональность; Q альтернатива порождению подклассов. Наследование поддерживает многообразие алгоритмов или поведений. Можно напрямую породить от Context подклассы с различными поведениями. Но при этом поведение жестко «зашивается» в класс Context. Вот почему реализации алгоритма и контекста смешиваются, что затрудняет понимание, сопровождение и расширение контекста. Кроме того, заменить алгоритм динамически уже не удастся. В результате вы получите множество родственных классов, отличающихся только алгоритмом или поведением. Инкапсуляции алгоритма в отдельный класс Strategy позволяют изменять его независимо от контекста; Q с помощью стратегий можно избавиться от условных операторов. Благодаря паттерну стратегия удается отказаться от условных операторов при выборе нужного поведения. Когда различные поведения помещаются в один класс, трудно выбрать нужное без применения условных операторов. Инкапсуляция же каждого поведения в отдельный класс Strategy решает эту проблему. Так, без использования стратегий код для разбиения текста на строки мог бы выглядеть следующим образом: void Composition: :Repair () { switch ( breakingStrategy) { case SimpleStrategy: ComposeWithSimpleCompositor () ; break; case TeXStrategy: ComposeWithTeXCompositor ( ) ; break; // если необходимо, объединить результаты с имеющейся // композицией } Паттерн же стратегия позволяет обойтись без оператора переключения за счет делегирования задачи разбиения на строки объекту Strategy: void Composition::Repair () { compositor- >Compose(); // если необходимо, объединить результаты / / с имеющейся композицией Паттерны поведения Если код содержит много условных операторов, то часто это признак того, что нужно применить паттерн стратегия; О выбор реализации. Стратегии могут предлагать различные реализации одного и того же поведения. Клиент вправе выбирать подходящую стратегию в зависимости от своих требований к быстродействию и памяти; Q клиенты должны <<знатъ» о различных стратегиях. Потенциальный недостаток этого паттерна в том, что для выбора подходящей стратегии клиент должен понимать, чем отличаются разные стратегии. Поэтому наверняка придется раскрыть клиенту некоторые особенности реализации. Отсюда следует, что паттерн стратегия стоит применять лишь тогда, когда различия в поведении имеют значение для клиента; Q обмен информацией между стратегией и контекстом. Интерфейс класса Strategy разделяется всеми подклассами Concrete Strategy — неважно, сложна или тривиальна их реализация. Поэтому вполне вероятно, что некоторые стратегии не будут пользоваться всей передаваемой им информацией, особенно простые. Это означает, что в отдельных случаях контекст создаст и проинициализирует параметры, которые никому не нужны. Если возникнет проблема, то между классами Strategy и Context придется установить более тесную связь; О увеличение числа объектов. Применение стратегий увеличивает число объектов в приложении. Иногда эти издержки можно сократить, если реализовать стратегии в виде объектов без состояния, которые могут разделяться несколькими контекстами. Остаточное состояние хранится в самом контексте и передается при каждом обращении к объекту-стратегии. Разделяемые стратегии не должны сохранять состояние между вызовами. В описании паттерна приспособленец этот подхбд обсуждается более подробно. Реализация Рассмотрим следующие вопросы реализации: Q определение интерфейсов классов Strategy и Context. Интерфейсы классов Strategy и Context могут обеспечить объекту класса ConcreteStrategy эффективный доступ к любым данным контекста, и наоборот. Например, Context передает данные в виде параметров операциям класса Strategy. Это разрывает тесную связь между контекстом и стратегией. При этом не исключено, что контекст будет передавать данные, которые стратегии не нужны. Другой метод - передать контекст в качестве аргумента, в таком случае стратегия будет запрашивать у него данные, или, например, сохранить ссылку на свой контекст, так что передавать вообще ничего не придется. И в том, и в другом случаях стратегия может запрашивать только ту информацию, которая реально необходима. Но тогда в контексте должен быть определен более развитый интерфейс к своим данным, что несколько усиливает связанность классов Strategy и Context. Какой подход лучше, зависит от конкретного алгоритма и требований, которые он предъявляет к данным; Паттерн Strategy 1НН1НЕШ а стратегии как параметры шаблона. В C++ для конфигурирования класса стратегией можно использовать шаблоны. Этот способ хорош, только если стратегия определяется на этапе компиляции и ее не нужно менять во время выполнения. Тогда конфигурируемый класс (например, Context) определяется в виде шаблона, для которого класс Strategy является параметром: template void Operation)) { theStrategy .DoAlgorithm( ) ; } private : AStrategy theStrategy; Затем этот класс конфигурируется классом Strategy в момент инстанци-рования: class MyStrategy { public: void DoAlgorithm( ) ; I. Context При использовании шаблонов отпадает необходимость в абстрактном классе для определения интерфейса Strategy. Кроме того, передача стратегии в виде параметра шаблона позволяет статически связать стратегию с контекстом, вследствие чего повышается эффективность программы; и объекты-стратегии можно не задавать. Класс Context разрешается упростить, если для него отсутствие какой бы то ни было стратегии является нормой. Прежде чем обращаться к объекту Strategy, объект Context проверяет наличие стратегии. Если да, то работа продолжается как обычно, в противном случае контекст реализует некое поведение по умолчанию. Достоинство такого подхода в том, что клиентам вообще не нужно иметь дело со стратегиями, если их устраивает поведение по умолчанию. Пример кода Из раздела «Мотивация» мы приведем фрагмент высокоуровневого кода, в основе которого лежат классы Composition и Compositor из библиотеки Interviews [LCI+92]. В классе Composition есть коллекция экземпляров класса Component, представляющих текстовые и графические элементы документа. Компоновщик, то есть некоторый подкласс класса Compositor, составляет из объектов-компонентов строки, реализуя ту или иную стратегию разбиения на строки. С каждым объектом ассоциирован его естественный размер, а также свойства растягиваемости и сжимаемости. Растягиваемость определяет, насколько можно увеличивать объект по сравнению с его естественным размером, а сжимаемость — насколько можно этот размер уменьшать. Композиция передает эти значения компоновщику, который использует их, чтобы найти оптимальное место для разрыва строки. EHUHIIH Паттерны поведения class Composition { public: Composition(Compositor*); void Repair(); private: Compositor* _compositor; Component* _components; // список компонентов int _componentCount; // число компонентов int _lineWidth; // ширина строки в композиции Composition int* _lineBreaks; // позиции точек разрыва строки // (измеренные в компонентах) int _lineCount; // число строк }; Когда возникает необходимость изменить расположение элементов, композиция запрашивает у компоновщика позиции точек разрыва строк. При этом она передает компоновщику три массива, в которых описаны естественные размеры, растягиваемость и сжимаемость компонентов. Кроме того, передается число компонентов, ширина строки и массив, в который компоновщик должен поместить позиции точек разрыва. Компоновщик возвращает число рассчитанных им точек разрыва. Интерфейс класса Compositor позволяет композиции передать компоновщику всю необходимую ему информацию. Приведем пример передачи данных стратегии: class Compositor { public: virtual int Compose( Coord natural [], Coord stretch.!]. Coord shrink [] , int componentCount, int lineWidth, int breaks[] protected: Compositor(); }; Заметим, что Compositor - это абстрактный класс. В его конкретных подклассах определены различные стратегии разбиения на строки. Композиция обращается к своему компоновщику посредством операции Repair, которая прежде всего инициализирует массивы, содержащие естественные размеры, растягиваемость и сжимаемость каждого компонента (подробности мы опускаем). Затем Repair вызывает компоновщика для получения позиций точек разрыва и, наконец, отображает документ (этот код также опущен): void Composition::Repair () { Coord* natural; Coord* stretchability; Coord* shrinkability; int componentCount; int* breaks; Паттерн Strategy // подготовить массивы с характеристиками компонентов / // определить, где должны быть точки разрыва int breakCount; breakCount = _compositor->Compose( natural, stretchability, shrinkability, componentCount, _lineWidth, breaks // разместить компоненты с учетом точек разрыва Теперь рассмотрим подклассы класса Compositor. Класс SimpleCompositor для определения позиций точек разрыва исследует компоненты по одному: class SimpleCompositor : public Compositor { public: SimpleCompositor(); virtual int Compose( Coord natural[], Coord stretch[], Coord shrink[], int componentCount, int lineWidth, int breaks[] Класс TeXCompositor использует более глобальную стратегию. Он рассматривает абзац целиком, принимая во внимание размеры и растягиваемость компонентов. Данный класс также пытается равномерно «раскрасить» абзац, минимизируя ширину пропусков между компонентами: class TeXCompositor : public Compositor { public: TeXCompositor(); virtual int Compose( Coord natural[], Coord stretch[]. Coord shrink[], int componentCount, int lineWidth, int breaks[] ); // ... }; Класс ArrayCompositor разбивает компоненты на строки, оставляя между ними равные промежутки: class ArrayCompositor : public Compositor { public: ArrayCompositor(int interval); Паттерны поведения virtual int Compose( Coord natural!], Coord stretch!], Coord shrink!], int componentCount, int lineWidth, int breaks[] j He все из этих классов используют в полном объеме информацию, переданную операции Compose. SimpleComposiLor игнорирует растягиваемость компонентов, принимая во внимание только их естественную ширину. TeXCompositor использует всю переданную информацию, a ArrayCompositor игнорирует ее. При создании экземпляра класса Composition вы передаете ему компоновщик, которым собираетесь пользоваться: Composition* quick = new Composition (new SimpleCompositor) ; Composition* slick = new Composition (new TeXCompositor); Composition* iconic = new Composition (new ArrayCompositor (100) ); Интерфейс класса Compositor тщательно спроектирован для поддержки всех алгоритмов размещения, которые могут быть реализованы в подклассах. Вряд ли вам захочется изменять данный интерфейс при появлении каждого нового подкласса, поскольку это означало бы переписывание уже существующих подклассов. В общем случае именно интерфейсы классов Strategy и Context определяют, насколько хорошо паттерн стратегия соответствует своему назначению. Известные применения Библиотеки ЕТ++ [WGM88] и Interviews используют стратегии для инкапсуляции алгоритмов разбиения на строки - так, как мы только что видели. В системе RTL для оптимизации кода компиляторов [JML92] с помощью стратегий определяются различные схемы распределения регистров (RegisterAllocator) и политики управления потоком команд (RISCscheduler, CISCscheduler). Это позволяет гибко настраивать оптимизатор для разных целевых машинных архитектур. Каркас ЕТ++ SwapsManager предназначен для построения программ, рассчитывающих цены для различных финансовых инструментов [EG92]. Ключевыми абстракциями для него являются Instrument (инструмент) и YieldCurve (кривая дохода). Различные инструменты реализованы как подклассы класса Instrument. YieldCurve рассчитывает коэффициенты дисконтирования, на основе которых вычисляется текущее значение будущего движения ликвидности. Оба класса делегируют часть своего поведения объектам-стратегиям класса Strategy. В каркасе присутствует семейство конкретных стратегий для генерирования движения ликвидности, оценки оборотов и вычисления коэффициентов дисконтирования. Можно создавать новые механизмы расчетов, конфигурируя классы Instrument и YieldCurve другими объектами конкретных стратегий. Этот подход поддерживает как использование существующих реализаций стратегий в различных сочетаниях, так и определение новых. В библиотеке компонентов Грейди Буча [BV90] стратегии используются как аргументы шаблонов. В классах коллекций поддерживаются три разновидности Паттерн Strategy стратегий распределения памяти: управляемая (распределение из пула), контролируемая (распределение и освобождение защищены замками) и неуправляемая (стандартное распределение памяти). Стратегия передается классу коллекции в виде аргумента шаблона в момент его инстанцирования. Например, коллекция UnboundedCollection, в которой используется неуправляемая стратегия, ин-станцируется как UnboundedCollection RApp - это система для проектирования топологии интегральных схем [GA89, AG90]. Задача RApp - проложить провода между различными подсистемами на схеме. Алгоритмы трассировки в RApp определены как подклассы абстрактного класса Router, который является стратегией. В библиотеке ObjectWindows фирмы Borland [Вог94] стратегии используются в диалоговых окнах для проверки правильности введенных пользователем данных. Например, можно контролировать, что число принадлежит заданному диапазону, а в данном поле должны быть только цифры. Не исключено, что при проверке корректности введенной строки потребуется поиск данных в справочной таблице. Для инкапсуляции стратегий проверки в ObjectWindows используются объекты класса Validator — частный случай паттерна стратегия. Поля для ввода данных делегируют стратегию контроля необязательному объекту Validator. Клиент при необходимости присоединяет таких проверяющих к полю (пример необязательной стратегии). В момент закрытия диалогового окна поля «просят» своих контролеров проверить правильность данных. В библиотеке имеются классы контролеров для наиболее распространенных случаев, например RangeValidator для проверки принадлежности числа диапазону. Но клиент может легко определить и собственные стратегии проверки, порождая подклассы от класса Validator. Родственные паттерны Приспособленец: объекты-стратегии в большинстве случаев подходят как приспособленцы. Паттерн Template Method Название и классификация паттерна Шаблонный метод — паттерн поведения классов. Назначение Шаблонный метод определяет основу алгоритма и позволяет подклассам переопределить некоторые шаги алгоритма, не изменяя его структуру в целом. Мотивация Рассмотрим каркас приложения, в котором имеются классы Application и Document. Класс Application отвечает за открытие существующих документов, хранящихся во внешнем формате, например в виде файла. Объект класса Document представляет информацию документа после его прочтения из файла. Приложения, построенные на базе этого каркаса, могут порождать подклассы от классов Application и Document, отвечающие конкретным потребностям. Например, графический редактор определит подклассы DrawApplication Паттерны поведения и DrawDocument, а электронная таблица — подклассы SpreadsheetApplication и SpreadsheetDocument. В абстрактном классе Application определен алгоритм открытия и считывания документа в операции OpenDocument: void Application::OpenDocument (const char* name) { if (!CanOpenDocument(name)) { // работа с этим документом невозможна return; Document* doc = DoCreateDocument() if (doc) { _docs->AddDocument(doc); AboutToOpenDocument(doc); doc->0pen(); doc->DoRead(); "1 ) Операция OpenDocument определяет все шаги открытия документа. Она проверяет, можно ли открыть документ, создает объект класса Document, добавляет его к набору документов и считывает документ из файла. Операцию вида OpenDocument мы будем называть шаблонным методом, описывающим алгоритм в терминах абстрактных операций, которые замещены в подклассах для получения нужного поведения. Подклассы класса Appl icat ion выполняют проверку возможности открытия (CanOpenDocument) и создания документа (DoCreateDocument). Подклассы класса Document считывают документ (DoRead). Шаблонный метод определяет также операцию, которая позволяет подклассам Application получить информацию о том, что документ вот-вот будет открыт (AboutToOpenDocument). Определяя некоторые шаги алгоритма с помощью абстрактных операций, шаблонный метод фиксирует их последовательность, но позволяет реализовать их в подклассах классов Application и Document. Паттерн Template Method Применимость Паттерн шаблонный метод следует использовать: а чтобы однократно использовать инвариантные части алгоритма, оставляя реализацию изменяющегося поведения на усмотрение подклассов; а когда нужно вычленить и локализовать в одном классе поведение, общее для всех подклассов, дабы избежать дублирования кода. Это хороший пример техники «вынесения за скобки с целью обобщения», описанной в работе Уильяма Опдайка (William Opdyke) и Ральфа Джонсона (Ralph Johnson) [OJ93]. Сначала идентифицируются различия в существующем коде, а затем они выносятся в отдельные операции. В конечном итоге различающиеся фрагменты кода заменяются шаблонным методом, из которого вызываются новые операции; а для управления расширениями подклассов. Можно определить шаблонный метод так, что он будет вызывать операции-зацепки (hooks) - см. раздел «Результаты» - в определенных точках, разрешив тем самым расширение только в этих точках. Структура Участники a AbstractClass (Application) - абстрактный класс:
a ConcreteClass (MyApplication) - конкретный класс: - реализует примитивные операции, выполняющие шаги алгоритма спосо бом, который зависит от подкласса. Отношения ConcreteClass предполагает, что инвариантные шаги алгоритма будут выполнены в AbstractClass. Паттерны поведения Результаты Шаблонные методы - один из фундаментальных приемов повторного использования кода. Они особенно важны в библиотеках классов, поскольку предоставляют возможность вынести общее поведение в библиотечные классы. Шаблонные методы приводят к инвертированной структуре кода, которую иногда называют принципом Голливуда, подразумевая часто употребляемую в этой киноимперии фразу «Не звоните нам, мы сами позвоним» [Swe85]. В данном случае это означает, что родительский класс вызывает операции подкласса, а не наоборот. Шаблонные методы вызывают операции следующих видов: а конкретные операции (либо из класса ConcreteClass, либо из классов клиента); а конкретные операции из класса AbstractClass (то есть операции, полезные всем подклассам); а примитивные операции (то есть абстрактные операции); а фабричные методы (см. паттерн фабричный метод); а операции-зацепки (hook operations), реализующие поведение по умолчанию, которое может быть расширено в подклассах. Часто такая операция по умолчанию не делает ничего. Важно, чтобы в шаблонном методе четко различались операции-зацепки (которые можно замещать) и абстрактные операции (которые нужно замещать). Чтобы повторно использовать абстрактный класс с максимальной эффективностью, авторы подклассов должны понимать, какие операции предназначены для замещения. Подкласс может расширить поведение некоторой операции, заместив ее и явно вызвав эту операцию из родительского класса: void DerivedClass::Operation () { Parent Class ::Operation(); // Расширенное поведение класса Periveddass } К сожалению, очень легко забыть о необходимости вызывать унаследованную операцию. У нас есть возможность трансформировать такую операцию в шаблонный метод с целью предоставить родителю контроль над тем, как подклассы расширяют его. Идея в том, чтобы вызывать операцию-зацепку из шаблонного метода в родительском классе. Тогда подклассы смогут переопределить именно эту операцию: void ParentClass::Operation () { // Поведение родительского класса ParentClass HookOperation(); } В родительском классе ParentClass операция HookOperation не делает ничего: void ParentClass::HookOperation () { } Паттерн Template Method Но она замещена в подклассах, которые расширяют поведение: void DerivedClass::HookOperation () { // расширение в производном классе } Реализация Стоит рассказать о трех аспектах, касающихся реализации: а использование контроля доступа в C++. В этом языке примитивные операции, которые вызывает шаблонный метод, можно объявить защищенными членами. Тогда гарантируется, что вызывать их сможет только сам шаблонный метод. Примитивные операции, которые обязательно нужно замещать, объявляются как чисто виртуальные функции. Сам шаблонный метод замещать не надо, так что его можно сделать невиртуальной функцией-членом; а сокращение числа примитивных операций. Важной целью при проектировании шаблонных методов является всемерное сокращение числа примитивных операций, которые должны быть замещены в подклассах. Чем больше операций нужно замещать, тем утомительнее становится программирование клиента; а соглашение об именах. Выделить операции, которые необходимо заместить, можно путем добавления к их именам некоторого префикса. Например, в каркасе МасАрр для приложений на платформе Macintosh [App89] имена шаблонных методов начинаются с префикса Do: DoCreateDocument, DoRead и т.д. Пример кода Следующий написанный на C++ пример показывает, как родительский класс может навязать своим подклассам некоторый инвариант. Пример взят из библиотеки NeXT Ap p Kit [Add94]. Рассмотрим класс View, поддерживающий рисование на экране, - своего рода инвариант, заключающийся в том, что подклассы могут изменять вид только тогда, когда он находится в фокусе. Для этого необходимо, чтобы был установлен определенный контекст рисования (например, цвета и шрифты). Чтобы установить состояние, можно использовать шаблонный метод Display. В классе View определены две конкретные операции (SetFocus и ResetFocus), которые соответственно устанавливают и сбрасывают контекст рисования. Операция-зацепка DoDisplay класса View занимается собственно рисованием. Display вызывает SetFocus перед DoDisplay, чтобы подготовить контекст, и ResetFocus после DoDisplay - чтобы его сбросить: void View::Display () { SetFocus(); DoDisplay(); ResetFocus'() ; Паттерны поведения С целью поддержки инварианта клиенты класса.View всегда вызывают Display и подклассы View всегда замещают DoDisplay. В классе View операция DoDisplay не делает ничего: void View::DoDisplay () { } Чтобы она что-то рисовала, подклассы переопределяют ее: void MyView: : DoDisplay О { // изобразить содержимое вида Известные применения Шаблонные методы настолько фундаментальны, что встречаются почти в каждом абстрактном классе. В работах Ребекки Вирфс-Брок и др. [WBWW90, WBJ90] подробно обсуждаются шаблонные методы. Родственные паттерны Фабричные методы часто вызываются из шаблонных. В примере из раздела «Мотивация» шаблонный метод OpenDocument вызывал фабричный метод DoCreateDocument. Стратегия: шаблонные методы применяют наследование для модификации части алгоритма. Стратегии используют делегирование для модификации алгоритма в целом. Паттерн Visitor Название и классификация паттерна Посетитель - паттерн поведения объектов. Назначение Описывает операцию, выполняемую с каждым объектом из некоторой структуры. Паттерн посетитель позволяет определить новую операцию, не изменяя классы этих объектов. Мотивация Рассмотрим компилятор, который представляет программу в виде абстрактного синтаксического дерева. Над такими деревьями он должен выполнять операции «статического семантического» анализа, например проверять, что все переменные определены. Еще ему нужно генерировать код. Аналогично можно было бы определить операции контроля типов, оптимизации кода, анализа потока выполнения, проверки того, что каждой переменной было присвоено конкретное значение перед первым использованием, и т.д. Более того, абстрактные синтаксические деревья могли бы служить для красивой печати программы, реструктурирования кода и вычисления различных метрик программы. В большинстве таких операций узлы дерева, представляющие операторы присваивания, следует рассматривать иначе, чем узлы, представляющие переменные и арифметические выражения. Поэтому один класс будет создан для операторов Паттерн Visitor присваивания, другой - для доступа к переменным, третий - для арифметических выражений и т.д. Набор классов узлов, конечно, зависит от компилируемого языка, но не очень сильно. На представленной диаграмме показана часть иерархии классов Node. Проблема здесь в том, что если раскидать все операции по классам различных узлов, то получится система, которую трудно понять, сопровождать и изменять. Вряд ли кто-нибудь разберется в программе, если код, отвечающий за проверку типов, будет перемешан с кодом, реализующим красивую печать или анализ потока выполнения. Кроме того, добавление любой новой операции потребует перекомпиляции всех классов. Оптимальный вариант - наличие возможности добавлять операции по отдельности и отсутствие зависимости классов узлов от применяемых к ним операций. И того, и другого можно добиться, если поместить взаимосвязанные операции из каждого класса в отдельный объект, называемый посетителем, и передавать его элементам абстрактного синтаксического дерева по мере обхода. «Принимая» посетителя, элемент посылает ему запрос, в котором содержится, в частности, класс элемента. Кроме того, в запросе присутствует в виде аргумента и сам элемент. Посетителю в данной ситуации предстоит выполнить операцию над элементом, ту самую, которая наверняка находилась бы в классе элемента. Например, компилятор, который не использует посетителей, мог бы проверить тип процедуры, вызвав операцию TypeCheck для представляющего ее абстрактного синтаксического дерева. Каждый узел дерева должен был реализовать операцию TypeCheck путем рекурсивного вызова ее же для своих компонентов (см. приведенную выше диаграмму классов). Если же компилятор проверяет тип процедуры посредством посетителей, то ему достаточно создать объект класса TypeCheckingVisitor и вызвать для дерева операцию Accept, передав ей этот объект в качестве аргумента. Каждый узел должен был реализовать Accept путем обращения к посетителю: узел, соответствующий оператору присваивания, вызывает операцию посетителя Visit Assignment, а узел, ссылающийся на переменную, - операцию VisitVariableRef erence. To, что раньше было операцией TypeCheck в классе AssignmentNode, стало операцией VisitAssignment в классе TypeCheckingVisitor. ; Паттерны поведения Чтобы посетители могли заниматься не только проверкой типов, нам необходим абстрактный класс Nodevisitor, являющийся родителем для всех посетителей синтаксического дерева. Приложение, которому нужно вычислять метрики программы, определило бы новые подклассы Nodevisitor, так что нам не пришлось бы добавлять зависящий от приложения код в классы узлов. Паттерн посетитель инкапсулирует операции, выполняемые на каждой фазе компиляции, в классе Visitor, ассоциированном с этой фазой. Применяя паттерн посетитель, вы определяете две иерархии классов: одну для элементов, над которыми выполняется операция (иерархия Node), а другую - для посетителей, описывающих те операции, которые выполняются над элементами (иерархия NodeVisitor). Новая операция создается путем добавления подкласса в иерархию классов посетителей. До тех пор пока грамматика языка остается постоянной (то есть не добавляются новые подклассы Node), новую функциональность можно получить путем определения новых подклассов NodeVisitor. Применимость Используйте паттерн посетитель, когда: а в структуре присутствуют объекты многих классов с различными интерфейсами и вы хотите выполнять над ними операции, зависящие от конкретных классов; Паттерн Visitor а над объектами, входящими в состав структуры, надо выполнять разнообразные, не связанные между собой операции и вы не хотите «засорять» классы такими операциями. Посетитель позволяет объединить родственные операции, поместив их в один класс. Если структура объектов является общей для нескольких приложений, то паттерн посетитель позволит в каждое приложение включить только относящиеся к нему операции; а классы, устанавливающие структуру объектов, изменяются редко, но новые операции над этой структурой добавляются часто. При изменении классов, представленных в структуре, нужно будет переопределить интерфейсы всех посетителей, а это может вызвать затруднения. Поэтому если классы меняются достаточно часто, то, вероятно, лучше определить операции прямо в них. Структура Участники a Visitor (NodeVisitor) - посетитель: - объявляет операцию Visit для каждого класса ConcreteElement в структуре объектов. Имя и сигнатура этой операции идентифицируют класс, который посылает посетителю запрос Visit. Это позволяет посе тителю определить, элемент какого конкретного класса он посещает. Вла дея такой информацией, посетитель может обращаться к элементу напря мую через его интерфейс; a ConcreteVisitor (TypeCheckingVisitor) - конкретный посетитель: - реализует все операции, объявленные в классе Visitor. Каждая операция реализует фрагмент алгоритма, определенного для класса соответствующего Паттерны поведения объекта в структуре. Класс ConcreteVisitor предоставляет контекст для этого алгоритма и сохраняет его локальное состояние. Часто в этом состоянии аккумулируются результаты, полученные в процессе обхода структуры; a Element (Node) - элемент: - определяет операцию Accept, которая принимает посетителя в качестве аргумента; a ConcreteElement (AssignmentNode, VariableRefNode) - конкретный элемент:
Отношения а клиент, использующий паттерн посетитель, должен создать объект класса ConcreteVisitor, азатем обойти всю структуру, посетив каждый ее элемент. а при посещении элемента последний вызывает операцию посетителя, соответствующую своему классу. Элемент передает этой операции себя в качестве аргумента, чтобы посетитель мог при необходимости получить доступ к его состоянию. На представленной диаграмме взаимодействий показаны отношения между объектом, структурой, посетителем и двумя элементами. Результаты Некоторые достоинства и недостатки паттерна посетитель: Q упрощает добавление новых операций. С помощью посетителей легко добавлять операции, зависящие от компонентов сложных объектов. Для определения |