Э. Гамма Р. Хелм Р. Джонсон Дж. Влиссидес

Вид материалаДокументы

Содержание


Паттерны поведения
Паттерн State
Пример кода
TCPConnection: : TCPConnection
Таким образом, каждый подкласс TCPState - это одиночка.
Известные применения
Родственные паттерны
Паттерн Strategy
Известен также под именем
Q SimpleCompositor
Q ArrayCompositor
Q ConcreteStrategy
Паттерн Strategy
О семейства родственных алгоритмов.
Паттерн Strategy 1НН1НЕШ
Пример кода
EHUHIIH Паттерны поведения
Паттерн Strategy
Известные применения
Родственные паттерны
...
Полное содержание
Подобный материал:
1   ...   12   13   14   15   16   17   18   19   20

Паттерны поведения

много, то такое распределение эффективнее, так как в противном случае пришлось бы иметь дело с громоздкими условными операторами. Наличие громоздких условных операторов нежелательно, равно как и нали­чие длинных процедур. Они слишком монолитны, вот почему модификация и расширение кода становится проблемой. Паттерн состояние предлагает более удачный способ структурирования зависящего от состояния кода. Ло­гика, описывающая переходы между состояниями, больше не заключена в монолитные операторы 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) - контекст:
  • конфигурируется объектом класса ConcreteStrategy;
  • хранит ссылку на объект класса Strategy;
  • может определять интерфейс, который позволяет объекту Strategy по­
    лучить доступ к данным контекста.

Отношения

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 class Context {

void Operation)) { theStrategy .DoAlgorithm( ) ; }

private :

AStrategy theStrategy;

Затем этот класс конфигурируется классом Strategy в момент инстанци-рования:

class MyStrategy { public:

void DoAlgorithm( ) ; I.

Context aContext;

При использовании шаблонов отпадает необходимость в абстрактном клас­се для определения интерфейса 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) - абстрактный класс:
  • определяет абстрактные примитивные операции, замещаемые в конкрет­
    ных подклассах для реализации шагов алгоритма;
  • реализует шаблонный метод, определяющий скелет алгоритма. Шаблон­
    ный метод вызывает примитивные операции, а также операции, опреде­
    ленные в классе AbstractClass или в других объектах;

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) - конкретный элемент:
  • реализует операцию Accept, принимающую посетителя как аргумент;
    a ObjectStructure (Program) - структура объектов:
  • может перечислить свои элементы;



  • может предоставить посетителю высокоуровневый интерфейс для посе­
    щения своих элементов;
  • может быть как составным объектом (см. паттерн компоновщик), так
    и коллекцией, например списком или множеством.

Отношения

а клиент, использующий паттерн посетитель, должен создать объект класса ConcreteVisitor, азатем обойти всю структуру, посетив каждый ее элемент.

а при посещении элемента последний вызывает операцию посетителя, соот­ветствующую своему классу. Элемент передает этой операции себя в каче­стве аргумента, чтобы посетитель мог при необходимости получить доступ к его состоянию.

На представленной диаграмме взаимодействий показаны отношения меж­ду объектом, структурой, посетителем и двумя элементами.




Результаты

Некоторые достоинства и недостатки паттерна посетитель:

Q упрощает добавление новых операций. С помощью посетителей легко добавлять операции, зависящие от компонентов сложных объектов. Для определения