346
Глава 5. Паттерны поведения
Этой ловушки можно избежать, отправляя уведомления из шаблонных методов (см. описание паттерна шаблонный метод (373)) абстрактно- го класса
Subject
. Определите примитивную операцию, замещаемую в подклассах, и обратитесь к
Notify
, используя последнюю операцию в шаблонном методе. В таком случае существует гарантия, что со- стояние объекта непротиворечиво, если операции
Subject замещены в подклассах:
void Text::Cut (TextRange r) {
ReplaceRange(r); // Переопределяется в подклассах
Notify();
}
Кстати, всегда желательно фиксировать, какие операции класса
Subject инициируют обновления;
предотвращение зависимости протокола обновления от наблюдате-
ля: модели вытягивания и проталкивания. В реализациях паттерна наблюдатель субъект довольно часто транслирует всем подписчикам дополнительную информацию о характере изменения. Она передает- ся в виде аргумента операции
Update
, и объем ее меняется в широких диапазонах.
На одном полюсе находится так называемая модель проталкивания (push model), когда субъект посылает наблюдателям детальную информацию об изменении независимо от того, нужно ли им это. На другом — модель
вытягивания (pull model), когда субъект не посылает ничего, кроме ми- нимального уведомления, а наблюдатели запрашивают детали позднее.
Модель вытягивания подчеркивает неинформированность субъекта о сво- их наблюдателях, а в модели проталкивания предполагается, что субъект владеет определенной информацией о потребностях наблюдателей. В слу- чае применения модели проталкивания степень повторного их использо- вания может снизиться, так как классы
Subject делают предположения о классах
Observer
, которые могут оказаться неправильными. С другой стороны, модель вытягивания может оказаться неэффективной, ибо на- блюдателям без помощи субъекта необходимо выяснять, что изменилось;
явное определение модификаций, представляющих интерес.Эффектив- ность обновления можно повысить, расширив интерфейс регистрации субъекта, то есть предоставив возможность при регистрации наблю- дателя указать, какие именно события его интересуют. Когда событие происходит, субъект информирует лишь тех наблюдателей, которые
Паттерн Observer (наблюдатель)
347про явили к нему интерес. Чтобы получать конкретное событие, наблю- датели присоединяются к своим субъектам следующим образом:
void Subject::Attach(Observer*, Aspect& interest);
где interest определяет представляющее интерес событие. В момент отправки уведомления субъект передает своим наблюдателям изменив- шийся аспект в виде параметра операции
Update
. Например:
void Observer::Update(Subject*, Aspect& interest);
инкапсуляция сложной семантики обновления. Если отношения за- висимости между субъектами и наблюдателями становятся особенно сложными, то может потребоваться объект, инкапсулирующий эти от- ношения. Будем называть его
ChangeManager
(менеджер изменений). Он должен свести к минимуму объем работы, необходимой для того, чтобы наблюдатели смогли отразить изменения субъекта. Например, если не- которая операция влечет за собой изменения в нескольких независимых субъектах, то хотелось бы, чтобы наблюдатели уведомлялись после того, как будут модифицированы
все субъекты, дабы не ставить в известность одного и того же наблюдателя несколько раз.
У класса
ChangeManager есть три обязанности:
• строить отображение между субъектом и его наблюдателями и предо- ставлять интерфейс для поддержания отображения в актуальном состоянии. Это освобождает субъектов от необходимости хранить ссылки на своих наблюдателей и наоборот;
•
определять конкретную стратегию обновления;
• обновлять всех зависимых наблюдателей по запросу от субъекта.
На следующей схеме представлена простая реализация паттерна наблюда- тель с использованием менеджера изменений
ChangeManager
. Имеется два специализированных менеджера.
SimpleChangeManager всегда обновляет всех наблюдателей каждого субъекта, а
DAGChangeManager обрабатывает направленные ациклические графы зависимостей между субъектами и их наблюдателями. Когда наблюдатель должен «присматривать» за не- сколькими субъектами, предпочтительнее использовать
DAGChangeManagerВ этом случае изменение сразу двух или более субъектов может привести к избыточным обновлениям. Объект
DAGChangeManager гарантирует, что наблюдатель в любом случае получит только одно уведомление. Если об- новление одного и того же наблюдателя допускается несколько раз подряд, то вполне достаточно объекта
SimpleChangeManager
348
Глава 5. Паттерны поведения
Observer
Update(Subject)
Register(Subject, Observer)
Unregister(Subject, Observer)
Notify()
ChangeManager
Subject–Observer mapping
Register(Subject, Observer)
Unregister(Subject, Observer)
Notify()
SimpleChangeManager
Register(Subject, Observer)
Unregister(Subject, Observer)
Notify()
DAGChangeManager
Пометить всех наблюдателей для обновления
Обновить всех помеченных наблюдателей
Для всех субъектов s
Для всех наблюдателей о в s o–>Update(s)
chman–>Register(this,o)
chman–>Notify()
Attach(Observer o)
Detach(Observer)
Notify()
Subject
subjects chman observers
ChangeManager
— это пример паттерна посредник (319). В общем случае есть только один объект
ChangeManager
, известный всем участникам. По- этому полезен будет также и паттерн одиночка (157);
комбинирование классов Subject и Observer. В библиотеках классов, кото- рые написаны на языках, не поддерживающих множественного насле- дования (например, на Smalltalk), обычно не определяются отдельные классы
Subject и
Observer
. Их интерфейсы комбинируются в одном классе. Это позволяет определить объект, выступающий в роли одно- временно субъекта и наблюдателя, без множественного наследования.
Так, в Smalltalk интерфейсы
Subject и
Observer определены в корневом классе
Object и потому доступны вообще всем классам.
Пример кода
Интерфейс наблюдателя определен в абстрактном классе
Observer
:
class Subject;
class Observer {
public:
virtual Observer();
virtual void Update(Subject* theChangedSubject) = 0;
protected:
Observer();
};
Паттерн Observer (наблюдатель)
349
Такая реализация поддерживает несколько субъектов для одного наблюдате- ля. Передача субъекта в параметре операции
Update позволяет наблюдателю определить, какой из наблюдаемых им субъектов изменился.
Аналогичным образом в абстрактном классе
Subject определен интерфейс субъекта:
class Subject {
public:
virtual Subject();
virtual void Attach(Observer*);
virtual void Detach(Observer*);
virtual void Notify();
protected:
Subject();
private:
List *_observers;
};
void Subject::Attach (Observer* o) {
_observers->Append(o);
}
void Subject::Detach (Observer* o) {
_observers->Remove(o);
}
void Subject::Notify () {
ListIterator i(_observers);
for (i.First(); !i.IsDone(); i.Next()) {
i.CurrentItem()->Update(this);
}
}
ClockTimer
— это конкретный субъект, который следит за временем суток. Он оповещает наблюдателей каждую секунду. Класс
ClockTimer предоставляет интерфейс для получения отдельных компонентов времени: часа, минуты, секунды и т. д.:
class ClockTimer : public Subject {
public:
ClockTimer();
virtual int GetHour();
virtual int GetMinute();
virtual int GetSecond();
void Tick();
};
350
Глава 5. Паттерны поведения
Операция
Tick вызывается через одинаковые интервалы внутренним тай- мером. Тем самым обеспечивается правильный отсчет времени. При этом обновляется внутреннее состояние объекта
ClockTimer и вызывается опе- рация
Notify для извещения наблюдателей об изменении:
void ClockTimer::Tick () {
// Обновить внутреннее представление времени
// ...
Notify();
}
Теперь можно определить класс
DigitalClock для вывода времени. Свою графическую функциональность он наследует от класса
Widget
, предо- ставляемого библиотекой для построения пользовательских интерфейсов.
Интерфейс наблюдателя примешивается к интерфейсу
DigitalClock путем наследования от класса
Observer
:
class DigitalClock: public Widget, public Observer {
public:
DigitalClock(ClockTimer*);
virtual DigitalClock();
virtual void Update(Subject*);
// Замещает операцию класса Observer virtual void Draw();
// Замещает операцию класса Widget;
// определяет способ отображения часов private:
ClockTimer* _subject;
};
DigitalClock::DigitalClock (ClockTimer* s) {
_subject = s;
_subject->Attach(this);
}
DigitalClock:: DigitalClock () {
_subject->Detach(this);
}
Прежде чем начнется рисование часов посредством операции
Update
, будет проверено, что уведомление получено именно от объекта таймера:
void DigitalClock::Update (Subject* theChangedSubject) {
if (theChangedSubject == _subject) {
Draw();
Паттерн Observer (наблюдатель)
351
}
}
void DigitalClock::Draw () {
// Получить новые значения от субъекта int hour = _subject->GetHour();
int minute = _subject->GetMinute();
// etc.
// Нарисовать цифровые часы
}
Аналогичным образом определяется класс
AnalogClock
:
class AnalogClock : public Widget, public Observer {
public:
AnalogClock(ClockTimer*);
virtual void Update(Subject*);
virtual void Draw();
// ...
};
Следующий код создает объекты классов
AnalogClock и
DigitalClock
, кото- рые всегда показывают одно и то же время:
ClockTimer* timer = new ClockTimer;
AnalogClock* analogClock = new AnalogClock(timer);
DigitalClock* digitalClock = new DigitalClock(timer);
При каждом срабатывании таймера timer оба экземпляра часов обновляются и перерисовывают себя.
Известные применения
Первый и, возможно, самый известный пример паттерна наблюдатель поя- вился в схеме «модель/представление/контроллер» (MVC) языка Smalltalk, которая представляет собой каркас для построения пользовательских ин- терфейсов в среде Smalltalk [KP88]. Класс
Model в MVC — субъект, а
View
— базовый класс для наблюдателей. В языках Smalltalk, ET++ [WGM88] и библиотеке классов THINK [Sym93b] предлагается общий механизм зависимостей, в котором интерфейсы субъекта и наблюдателя помещены в класс, являющийся общим родителем всех остальных системных классов.
Среди других библиотек для построения интерфейсов пользователя, в кото- рых используется паттерн наблюдатель, стоит упомянуть InterViews [LVC89],
352
Глава 5. Паттерны поведения
Andrew Toolkit [P+88] и Unidraw [VL90]. В InterViews явно определены классы
Observer и
Observable
(для субъектов). В библиотеке Andrew они называются представлением (view) и объектом данных (data object) соот- ветственно. Unidraw делит объекты графического редактора на части
View
(для наблюдателей) и
Subject
Родственные паттерны
Посредник (319): класс
ChangeManager действует как посредник между субъ- ектами и наблюдателями, инкапсулируя сложную семантику обновления.
Одиночка (157): класс
ChangeManager может воспользоваться паттерном одиночка, чтобы гарантировать уникальность и глобальную доступность менеджера изменений.
ПАТТЕРН STATE (СОСТОЯНИЕ)
Название и классификация паттерна
Состояние — паттерн поведения объектов.
Назначение
Позволяет объекту изменять свое поведение в зависимости от внутреннего состояния. Извне создается впечатление, что изменился класс объекта.
Мотивация
Рассмотрим класс
TCPConnection
, представляющий сетевое соединение.
Объект этого класса может находиться в одном из нескольких состояний:
Established
(установлено),
Listening
(прослушивание),
Closed
(закрыто).
Когда объект
TCPConnection получает запросы от других объектов, то в за- висимости от текущего состояния он отвечает по-разному. Например, ответ на запрос
Open
(открыть) зависит от того, находится ли соединение в состо- янии
Closed или
Established
. Паттерн состояние описывает, каким образом объект
TCPConnection может вести себя по-разному, находясь в различных состояниях.
Основная идея этого паттерна заключается в том, чтобы ввести абстракт- ный класс
TCPState для представления различных состояний соединения.
Этот класс объявляет интерфейс, общий для всех классов, описывающих различные рабочие состояния. В подклассах
TCPState реализовано по-
Паттерн State (состояние)
353
ведение, специ фичное для конкретного состояния. Например, в классах
TCPEstablished и
TCPClosed реализовано поведение, характерное для со- стояний
Established и
Closed соответственно.
TCPState
Open()
Close()
Acknowledge()
TCPListen
Open()
Close()
Acknowledge()
TCPClosed
Open()
Close()
Acknowledge()
TCPEstablished
Open()
Close()
Acknowledge()
TCPConnection
Open()
Close()
Acknowledge()
state–>Open()
state
Класс
TCPConnection хранит у себя объект состояния (экземпляр некоторого подкласса
TCPState
), представляющий текущее состояние соединения, и де- легирует все зависящие от состояния запросы этому объекту.
TCPConnection использует свой экземпляр подкласса
TCPState для выполнения операций, свойственных только данному состоянию соединения.
При каждом изменении состояния соединения
TCPConnection изменяет свой объект-состояние. Например, когда установленное соединение закрывает- ся,
TCPConnection заменяет экземпляр класса
TCPEstablished экземпляром
TCPClosed
Применимость
Основные условия для применения паттерна состояние:
поведение объекта зависит от его состояния и должно изменяться во время выполнения;
когда в коде операций встречаются состоящие из многих ветвей услов- ные операторы, в которых выбор ветви зависит от состояния. Обычно в таком случае состояние представлено перечисляемыми константами.
Часто одна и та же структура условного оператора повторяется в не- скольких операциях. Паттерн состояние предлагает поместить каждую ветвь в отдельный класс. Это позволяет трактовать состояние объекта как самостоятельный объект, который может изменяться независимо от других.
354 Глава 5. Паттерны поведения
Структура
ContextRequest()
state–>Handle()
state
StateHandle()ConcreteStateAHandle()
ConcreteStateBHandle()
Участники
Context (
TCPConnection
) — контекст:
• определяет интерфейс, представляющий интерес для клиентов;
• хранит экземпляр подкласса
ConcreteState
, которым определяется текущее состояние;
State (
TCPState
) — состояние:
• определяет интерфейс для инкапсуляции поведения, ассоциирован- ного с конкретным состоянием контекста
Context
;
Подклассы
ConcreteState
(
TCPEstablished
,
TCPListen
,
TCPClosed
) — конкретное состояние:
• каждый подкласс реализует поведение, ассоциированное с некоторым состоянием контекста
Context
Отношения
Класс
Context делегирует зависящие от состояния запросы текущему объекту
ConcreteState
;
контекст может передать себя в качестве аргумента объекту
State
, кото- рый будет обрабатывать запрос. Это дает возможность объекту-состоя- нию при необходимости получить доступ к контексту;
Context
— это основной интерфейс для клиентов. Клиенты могут кон- фигурировать контекст объектами состояния
State
.
Один раз сконфи- гурировав контекст, клиенты уже не должны напрямую связываться с объектами состояния;
либо
Context
, либо подклассы
ConcreteState могут решить, при каких условиях и в каком порядке происходит смена состояний.
Паттерн State (состояние)
355Результаты
Результаты использования паттерна состояние:
локализация поведения, зависящего от состояния, и деление его на части, соответствующие состояниям. Паттерн состояние помещает все пове- дение, ассоциированное с конкретным состоянием, в отдельный объект.
Поскольку зависящий от состояния код целиком находится в одном из подклассов класса
State
, то добавлять новые состояния и переходы можно просто путем порождения новых подклассов. Вместо этого мож- но было бы использовать данные-члены для определения внутренних состояний, тогда операции объекта
Context проверяли бы эти данные.
Но в таком случае похожие условные операторы или операторы ветвле- ния были бы разбросаны по всему коду класса
Context
. При этом добав- ление нового состояния потребовало бы изменения нескольких опера- ций, что затруднило бы сопровождение.
Паттерн состояние позволяет решить эту проблему, но одновременно порождает другую, поскольку поведение для различных состояний ока- зывается распределенным между несколькими подклассами
State
. Это увеличивает число классов. Конечно, один класс компактнее, но если со- стояний много, то такое распределение эффективнее, так как в противном случае пришлось бы иметь дело с громоздкими условными операторами.
Наличие громоздких условных операторов нежелательно, равно как и длинных процедур. Они
слишком монолитны, поэтому с модификацией и расширением кода возникают проблемы. Паттерн состояние предлагает более удачный способ структурирования зависящего от состояния кода.
Логика, описывающая переходы между состояниями, больше не заклю- чена в монолитные операторы if или switch
, а распределена между под- классами
State
. При инкапсуляции каждого перехода и действия в класс состояние становится полноценным объектом. Это улучшает структуру кода и проясняет его назначение;
явно выраженные переходы между состояниями. Если объект определя- ет свое текущее состояние исключительно в терминах внутренних дан- ных, то переходы между состояниями не имеют явного представления; они проявляются лишь как присваивания некоторым переменным. Ввод отдельных объектов для различных состояний делает переходы более явными. Кроме того, объекты
State могут защитить контекст
Context от рассогласования внутренних переменных, поскольку переходы с точки зрения контекста — это атомарные действия. Для осуществления пере- хода надо изменить значение только одной переменной (объектной пе- ременной
State в классе
Context
), а не нескольких [dCLF93];