Э. Гамма, Р. Хелм
Скачать 6.37 Mb.
|
356 Глава 5. Паттерны поведения возможность совместного использования объектов состояния. Если в объекте состояния State отсутствуют переменные экземпляра, то есть представляемое им состояние кодируется исключительно самим типом, то разные контексты могут разделять один и тот же объект State . Когда состояния разделяются таким образом, они являются, по сути дела, при- способленцами (см. описание паттерна приспособленец (231)), у кото- рых нет внутреннего состояния, а есть только поведение. Реализация При реализации паттерна состояние следует обратить внимание на следу- ющие аспекты: что определяет переходы между состояниями. Паттерн состояние ничего не сообщает о том, какой участник определяет критерий перехода между состояниями. Если критерии зафиксированы, то их можно реализовать непосредственно в классе Context . Однако в общем случае более гибкий и правильный подход заключается в том, чтобы позволить самим под- классам класса State определять следующее состояние и момент пере- хода. Для этого в класс Context надо добавить интерфейс, позволяющий объектам State установить состояние контекста. Такую децентрализо- ванную логику переходов проще модифицировать и расширять — нужно лишь определить новые подклассы State . Недостаток децентрализации в том, что каждый подкласс State должен знать еще хотя бы об одном подклассе, что вносит реализационные зависимости между подклассами; табличная альтернатива. Том Каргилл (Tom Cargill) в книге C++ Programming Style [Car92] описывает другой способ структурирования кода, управляемого состояниями. Он использует таблицу для отобра- жения входных данных на переходы между состояниями. С ее помощью можно определить, в какое состояние нужно перейти при поступлении некоторых входных данных. По существу, тем самым мы заменяем ус- ловный код (или виртуальные функции, если речь идет о паттерне со- стояние) поиском в таблице. Основное преимущество таблиц — в их регулярности: для изменения критериев перехода достаточно модифи- цировать только данные, а не код. Но есть и недостатки: • поиск в таблице часто менее эффективен, чем вызов функции (вир- туальной); • представление логики переходов в однородном табличном формате делает критерии менее явными и, стало быть, усложняет их понимание; • обычно трудно добавить действия, которыми сопровождаются переходы между состояниями. Табличный метод учитывает состояния и переходы Паттерн State (состояние) 357 между ними, но его необходимо дополнить, чтобы при каждом изме- нении состояния можно было выполнять произвольные вычисления. Главное различие между конечными автоматами на базе таблиц и паттер- ном состояние можно сформулировать так: паттерн состояние моделирует поведение, зависящее от состояния, а табличный метод акцентирует внимание на определении переходов между состояниями; создание и уничтожение объектов состояния. В процессе разработки обычно приходится выбирать между: (1) созданием объектов состоя- ния, когда в них возникает необходимость, и уничтожением сразу после использования, и (2) созданием их заранее и навсегда. Первый вариант предпочтителен в тех случаях, когда возможные состояния системы неизвестны заранее, а контекст изменяет состояние сравнитель- но редко. При этом объекты, которые никогда не будут использованы, не создаются, что может быть существенно, если в объектах состояния хра- нится много информации. Если изменения состояния происходят часто, и уничтожать представляющие их объекты было бы нежелательно (ибо они могут очень скоро понадобиться вновь), лучше воспользоваться вторым подходом. Время на создание объектов затрачивается только один раз, в самом начале, а на уничтожение — не затрачивается вовсе. Правда, этот вариант может оказаться неудобным, так как в контексте должны храниться ссылки на все состояния, в которых теоретически может оказаться система; использование динамического наследования. Изменение поведения по кон- кретному запросу может достигаться сменой класса объекта во время вы- полнения, но в большинстве объектно-ориентированных языков такая воз- можность не поддерживается. Исключение составляет Self [US87] и другие основанные на делегировании языки, которые предоставляют такой меха- низм и, следовательно, поддерживают паттерн состояние напрямую. Объ- екты в Self могут делегировать операции другим объектам, обеспечивая тем самым некую форму динамического наследования. Изменение целевого объекта делегирования во время выполнения фактически приводит к из- менению структуры графа наследования. Такой механизм позволяет объ- ектам варьировать поведение путем изменения своего класса. Пример кода В следующем примере приведен код на языке C++ с TCP-соединением из раз- дела «Мотивация». Это упрощенный вариант протокола TCP, в нем, конечно же, представлен не весь протокол и даже не все состояния TCP-соединений 1 1 Пример основан на описании протокола установления TCP-соединений из книги Линча и Роуза [LR93]. 358 Глава 5. Паттерны поведения Прежде всего определим класс TCPConnection , который предоставляет интер- фейс для передачи данных и обрабатывает запросы на изменение состояния: class TCPOctetStream; class TCPState; class TCPConnection { public: TCPConnection(); void ActiveOpen(); void PassiveOpen(); void Close(); void Send(); void Acknowledge(); void Synchronize(); void ProcessOctet(TCPOctetStream*); private: friend class TCPState; void ChangeState(TCPState*); private: TCPState* _state; }; В переменной _state класса TCPConnection хранится экземпляр класса TCPState . Этот класс дублирует интерфейс изменения состояния, определен- ный в классе TCPConnection . Каждая операция TCPState получает экземпляр TCPConnection в параметре, что позволяет объекту TCPState получить доступ к данным объекта TCPConnection и изменить состояние соединения: class TCPState { public: virtual void Transmit(TCPConnection*, TCPOctetStream*); virtual void ActiveOpen(TCPConnection*); virtual void PassiveOpen(TCPConnection*); virtual void Close(TCPConnection*); virtual void Synchronize(TCPConnection*); virtual void Acknowledge(TCPConnection*); virtual void Send(TCPConnection*); protected: void ChangeState(TCPConnection*, TCPState*); }; TCPConnection делегирует все запросы, зависящие от состояния, хранимому в _state экземпляру TCPState . Кроме того, в классе TCPConnection существует операция, с помощью которой в эту переменную можно записать указатель Паттерн State (состояние) 359 на другой объект 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); } void 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); } 360 Глава 5. Паттерны поведения В подклассах TCPState реализовано поведение, зависящее от состояния. Соединение TCP может находиться во многих состояниях: Established (установлено), Listening (прослушивание), Closed (закрыто) и т. д., и для каждого из них есть свой подкласс TCPState . Мы подробно рассмотрим три подкласса: TCPEstablished , TCPListen и TCPClosed : class TCPEstablished : public TCPState { public: static TCPState* Instance(); 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* Instance(); virtual void ActiveOpen(TCPConnection*); virtual void PassiveOpen(TCPConnection*); // ... }; В подклассах TCPState нет никакого локального состояния, поэтому они могут использоваться совместно, так что потребуется только по одному экземпляру каждого класса. Уникальный экземпляр подкласса TCPState создается обращением к статической операции Instance 1 В подклассах TCPState реализовано зависящее от состояния поведение для тех запросов, которые допустимы в этом состоянии: void TCPClosed::ActiveOpen (TCPConnection* t) { // Послать SYN, получить SYN, ACK и т. д. ChangeState(t, TCPEstablished::Instance()); } void TCPClosed::PassiveOpen (TCPConnection* t) { ChangeState(t, TCPListen::Instance()); } 1 Таким образом, каждый подкласс TCPState — это одиночка. Паттерн State (состояние) 361 void TCPEstablished::Close (TCPConnection* t) { // Послать FIN, получить ACK для FIN ChangeState(t, TCPListen::Instance()); } void TCPEstablished::Transmit ( TCPConnection* t, TCPOctetStream* o ) { t->ProcessOctet(o); } void TCPListen::Send (TCPConnection* t) { // Послать SYN, получить SYN, ACK и т. д. ChangeState(t, TCPEstablished::Instance()); } После выполнения действий, специфичных для своего состояния, эти опера- ции вызывают ChangeState для изменения состояния объекта TCPConnection У него нет никакой информации о протоколе TCP. Именно подклассы TCPState определяют переходы между состояниями и действия, диктуемые протоколом. Известные применения Ральф Джонсон и Джонатан Цвейг [JZ91] характеризуют паттерн состояние и описывают его применительно к протоколу TCP. Наиболее популярные интерактивные программы рисования предоставляют «инструменты» для наглядного выполнения операций на экране. Например, инструмент для рисования линий позволяет пользователю щелкнуть в про- извольной точке мышью, а затем, перемещая мышь, провести из этой точки линию. Инструмент выбора позволяет выбирать некоторые фигуры. Обычно все имеющиеся инструменты размещаются в палитре. Задача пользователя заключается в том, чтобы правильно выбрать и применить инструмент, но на самом деле поведение редактора изменяется при смене инструмента: при помощи инструмента для рисования мы создаем фигуры, при помощи инструмента выбора — выбираем их и т. д. Чтобы отразить зависимость поведения редактора от текущего инструмента, можно воспользоваться паттерном состояние. Можно определить абстрактный класс Tool , подклассы которого реализуют поведение, зависящее от инструмента. Графический редактор хранит ссылку на текущий объект Tool и делегирует ему поступающие запросы. При выборе инструмента редактор использует другой объект, что приводит к изменению поведения. Этот прием используется в каркасах графических редакторов HotDraw [Joh92] и Unidraw [VL90]. Он позволяет клиентам легко определять новые 362 Глава 5. Паттерны поведения виды инструментов. В HotDraw класс DrawingController переадресует запросы текущему объекту Tool . В Unidraw соответствующие классы на- зываются Viewer и Tool . На приведенной ниже схеме классов схематично представлены интерфейсы классов Tool и DrawingController CreationTool SelectionTool TextTool HandleMousePress() HandleMouseRelease() HandleCharacter() GetCursor() Activate() Tool MousePressed() ProcessKeyboard() Initialize() DrawingController currentTool Описанная Джеймсом Коплиеном [Cop92] идиома «конверт — письмо» (Envelope-Letter) также имеет отношение к паттерну состояние. По сути она представляет собой механизм изменения класса объекта во время выполне- ния. Паттерн состояние более конкретен; в нем акцент делается на работу с объектами, поведение которых зависит от состояния. Родственные паттерны Паттерн приспособленец (231) объясняет, как и когда можно совместно использовать объекты состояния. Объекты состояния часто бывают одиночками (157). ПАТТЕРН STRATEGY (СТРАТЕГИЯ) Название и классификация паттерна Стратегия — паттерн поведения объектов. Назначение Определяет семейство алгоритмов, инкапсулирует каждый из них и делает их взаимозаменяемыми. Стратегия позволяет изменять алгоритмы незави- симо от клиентов, которые ими пользуются. Паттерн Strategy (стратегия) 363 Другие названия Policy (политика). Мотивация Существует много алгоритмов для разбиения текста на строки. Жестко «зашивать» все подобные алгоритмы в классы, которые в них нуждаются, нежелательно по нескольким причинам: клиент, которому требуется алгоритм разбиения на строки, усложня- ется при включении в него соответствующего кода. Таким образом, клиенты становятся более громоздкими и создают больше сложностей в сопровождении, особенно если нужно поддерживать сразу несколько алгоритмов; в зависимости от обстоятельств могут применяться разные алгоритмы. Было бы неэффективно поддерживать несколько алгоритмов разбие- ния на строки, если мы не будем ими пользоваться; если разбиение на строки является неотъемлемой частью клиента, то задачи добавления новых и модификации существующих алгоритмов усложняются. Всех этих проблем можно избежать, если определить классы, инкапсули- рующие различные алгоритмы разбиения на строки. Инкапсулированный таким образом алгоритм называется стратегией. Compositor Compose() SimpleCompositor Compose() ArrayCompositor Compose() TeXCompositor Compose() Composition Traverse() Repair() compositor–>Compose compositor Допустим, класс Composition отвечает за разбиение на строки текста, ото- бражаемого в окне программы просмотра, и его своевременное обновление. Стратегии разбиения на строки определяются не в классе Composition , а в подклассах абстрактного класса Compositor . Несколько примеров: SimpleCompositor реализует простую стратегию, выделяющую по одной строке за раз; 364 Глава 5. Паттерны поведения TeXCompositor реализует алгоритм поиска точек разбиения на строки, принятый в редакторе TeX. Эта стратегия пытается оптимизировать разбиение на строки глобально, то есть в целом абзаце; ArrayCompositor реализует стратегию расстановки переходов на новую строку таким образом, что в каждой строке оказывается одно и то же число элементов. Например, это может быть полезно при построчном отображении набора пиктограмм. Объект Composition хранит ссылку на объект Compositor . Всякий раз, когда объекту Composition требуется переформатировать текст, он делегирует дан- ную обязанность своему объекту Compositor . Чтобы указать, какой объект Compositor должен использоваться, клиент встраивает его в объект Composition Применимость Основные условия для применения паттерна стратегия: наличие множества родственных классов, отличающихся только поведе- нием. Стратегия позволяет настроить класс одним из многих возмож- ных вариантов поведения; наличие нескольких разновидностей алгоритма. Например, можно опре- делить два варианта алгоритма, один из которых требует больше време- ни, а другой — больше памяти. Стратегии разрешается применять, когда варианты алгоритмов реализованы в виде иерархии классов [HO87]; в алгоритме содержатся данные, о которых клиент не должен «знать». Используйте паттерн стратегия, чтобы не раскрывать сложные, специ- фичные для алгоритма структуры данных; в классе определено много вариантов поведения, представленных разветв- ленными условными операторами. В этом случае проще перенести код из ветвей в отдельные классы стратегий. Структура strategy ConcreteStrategyA_AlgorithmInterface()ConcreteStrategyB_AlgorithmInterface()_Strategy'>ConcreteStrategyA AlgorithmInterface() ConcreteStrategyB AlgorithmInterface() Strategy AlgorithmInterface() Context ContextInterface() ConcreteStrategyC AlgorithmInterface() Паттерн Strategy (стратегия) 365 Участники Strategy ( Compositor ) — стратегия: • объявляет общий для всех поддерживаемых алгоритмов интерфейс. Класс Context пользуется этим интерфейсом для вызова конкретного алгоритма, определенного в классе ConcreteStrategy ; ConcreteStrategy ( SimpleCompositor , TeXCompositor , ArrayCompositor ) — конкретная стратегия: • реализует алгоритм, использующий интерфейс, объявленный в классе Strategy ; Context ( Composition ) — контекст: • настраивается объектом класса ConcreteStrategy ; • хранит ссылку на объект класса Strategy ; • может определять интерфейс, который позволяет объекту Strategy обращаться к данным контекста. Отношения Классы Strategy и Context взаимодействуют для реализации выбран- ного алгоритма. Контекст может передать стратегии все необходимые алгоритму данные в момент его вызова. Вместо этого контекст может позволить обращаться к своим операциям в нужные моменты, переда- вая ссылку на самого себя операциям класса Strategy ; контекст переадресует запросы своих клиентов объекту-стратегии. Обычно клиент создает объект ConcreteStrategy и передает его кон- тексту, после чего клиент взаимодействует исключительно с контек- стом. Часто в распоряжении клиента находится несколько классов ConcreteStrategy , которые он может выбирать. Результаты Основные достоинства и недостатки паттерна стратегия: семейства родственных алгоритмов. Иерархия классов Strategy опре- деляет семейство алгоритмов или вариантов поведения, которые можно повторно использовать в разных контекстах. Наследование позволяет вычленить общую для всех алгоритмов функциональность; альтернатива порождению подклассов. Наследование поддерживает многообразие алгоритмов или поведений. Можно напрямую породить от Context подклассы с различными поведениями. Но при этом поведение |