336
Глава 5. Паттерны поведения
Пример кода
Приведенный пример кода на языке C++ иллюстрирует рассмотренный выше пример класса
ConstraintSolver для разрешения ограничений. Мы используем объекты
MoveCommand
(см. паттерн команда (275)) для выпол- нения и отмены переноса графического объекта из одного места в другое.
Графический редактор вызывает операцию
Execute объекта-команды, чтобы переместить объект, и команду
Unexecute
, чтобы отменить перемещение.
В команде хранятся координаты места назначения, величина cмещения и экземпляр класса
ConstraintSolverMemento
— хранителя, содержащего состояние объекта
ConstraintSolver
:
class Graphic;
// Базовый класс графических объектов class MoveCommand {
public:
MoveCommand(Graphic* target, const Point& delta);
void Execute();
void Unexecute();
private:
ConstraintSolverMemento* _state;
Point _delta;
Graphic* _target;
};
Ограничения связанности устанавливаются классом
ConstraintSolver
Его основная функция
Solve обрабатывает ограничения, регистрируе- мые операцией
AddConstraint
. Для поддержки отмены действий состо- яние объекта
ConstraintSolver можно сохранить в экземпляре класса
ConstraintSolverMemento с помощью операции
CreateMemento
. В предыду- щее состояние объект
ConstraintSolver возвращается вызовом
SetMemento
ConstraintSolver является примером паттерна одиночка (157):
class ConstraintSolver {
public:
static ConstraintSolver* Instance();
void Solve();
void AddConstraint(
Graphic* startConnection, Graphic* endConnection
);
void RemoveConstraint(
Graphic* startConnection, Graphic* endConnection
);
ConstraintSolverMemento* CreateMemento();
Паттерн Memento (хранитель)
337 void SetMemento(ConstraintSolverMemento*);
private:
// Нетривиальное состояние и операции
// для поддержки семантики связанности
};
class ConstraintSolverMemento {
public:
virtual ConstraintSolverMemento();
private:
friend class ConstraintSolver;
ConstraintSolverMemento();
// Закрытое состояние Solver
};
С такими интерфейсами можно реализовать функции
Execute и
Unexecute в классе
MoveCommand следующим образом:
void MoveCommand::Execute () {
ConstraintSolver* solver = ConstraintSolver::Instance();
_state = solver->CreateMemento();
// Создание хранителя
_target->Move(_delta);
solver->Solve();
}
void MoveCommand::Unexecute () {
ConstraintSolver* solver = ConstraintSolver::Instance();
_target->Move(-_delta);
solver->SetMemento(_state);
// Восстановление состояния solver->Solve();
}
Execute запрашивает хранителя
ConstraintSolverMemento перед началом перемещения графического объекта.
Unexecute возвращает объект на преж- нее место, восстанавливает состояние
Solver и обращается к последнему с целью отменить ограничения.
Известные применения
Предыдущий пример основан на поддержке связанности в каркасе Unidraw с помощью класса
CSolver
[VL90].
В коллекциях языка Dylan [App92] для итерации предусмотрен интерфейс, напоминающий паттерн хранитель. Для этих коллекций существует понятие состояния объекта,
которое является хранителем, представляющим состоя- ние итерации. Представление текущего состояния каждой коллекции может
338
Глава 5. Паттерны поведения быть любым, но оно полностью скрыто от клиентов. Решение, используемое в языке Dylan, можно написать на C++ следующим образом:
template
class Collection {
public:
Collection();
IterationState* CreateInitialState();
void Next(IterationState*);
bool IsDone(const IterationState*) const;
Item CurrentItem(const IterationState*) const;
IterationState* Copy(const IterationState*) const;
void Append(const Item&);
void Remove(const Item&);
// ...
};
Операция
CreateInitialState возвращает инициализированный объект
IterationState для коллекции. Операция
Next переходит к следующему объекту в порядке итерации, фактически она увеличивает на единицу индекс итерации. Операция
IsDone возвращает true
, если в результате вы- полнения
Next мы оказались за последним элементом коллекции. Операция
CurrentItem разыменовывает объект состояния и возвращает тот элемент коллекции, на который он ссылается.
Copy возвращает копию данного объ- екта состояния. Это имеет смысл, когда необходимо оставить закладку в не- котором месте, пройденном во время итерации.
Для заданного класса
ItemType обход коллекции, составленной из его эк- земпляров, может выполняться так
1
:
class ItemType {
public:
void Process();
// ...
};
Collection aCollection;
IterationState* state;
state = aCollection.CreateInitialState();
1
Отметим, что в нашем примере объект состояния удаляется по завершении итерации.
Но оператор delete не будет вызван, если
ProcessItem возбудит исключение, поэтому в памяти остается мусор. Это проблема в языке C++, но не в Dylan, где есть сборщик мусора. Решение проблемы обсуждается на с. 258.
Паттерн Observer (наблюдатель)
339while (!aCollection.IsDone(state)) {
aCollection.CurrentItem(state)->Process();
aCollection.Next(state);
}
delete state;
У интерфейса итерации, основанного на паттерне хранитель, есть два пре- имущества:
с одной коллекцией может быть связано несколько активных состояний
(как и в случае с паттерном итератор (302));
поддержка итерации не требует нарушения инкапсуляции коллекции.
Хранитель интерпретируется только самой коллекцией, больше никто к нему доступа не имеет. При других подходах приходится нарушать ин- капсуляцию, объявляя классы итераторов друзьями классов коллекций
(см. описание паттерна итератор (302)). В случае с хранителем ситу- ация противоположная: класс коллекции
Collection является другом класса
IteratorState
В библиотеке QOCA для разрешения ограничений в хранителях содержится информация об изменениях.
Клиент может получить хранитель, характе- ризующий текущее решение системы ограничений. В хранителе находятся только те переменные ограничений, которые были преобразованы со времени последнего решения. Обычно при каждом новом решении изменяется лишь небольшое подмножество переменных
Solver
. Но этого достаточно, чтобы вернуть
Solver к предыдущему решению; для отката к более ранним реше- ниям необходимо иметь все промежуточные хранители. Поэтому передавать хранители в произвольном порядке нельзя; QOCA использует механизм ведения истории для возврата к прежним решениям.
Родственные паттерны
Команда (275): команды помещают информацию о состоянии, необходимую для отмены выполненных действий, в хранители.
Итератор (302): хранители могут использоваться для выполнения итераций, как было показано выше.
ПАТТЕРН OBSERVER (НАБЛЮДАТЕЛЬ)
Название и классификация паттерна
Наблюдатель — паттерн поведения объектов.
340
Глава 5. Паттерны поведения
Назначение
Определяет зависимость типа «один ко многим» между объектами таким образом, что при изменении состояния одного объекта все зависящие от него оповещаются об этом и автоматически обновляются.
Другие названия
Dependents (подчиненные), Publish-Subscribe (издатель — подписчик).
Мотивация
Одним из типичных побочных эффектов разбиения системы на взаимо- действующие классы является необходимость согласования состояния взаимосвязанных объектов. Однако согласованность не должна достигаться за счет жесткой связанности классов, так как это снижает возможности их повторного использования.
Например, во многих библиотеках для построения графических интерфейсов пользователя презентационные аспекты интерфейса отделены от данных приложения [KP88, LVC89, P+88, WGM88]. С классами, описывающими данные и их представление, можно работать автономно; при этом они могут работать и совместно. Электронная таблица и объект-диаграмма не имеют информации друг о друге, поэтому их можно использовать по отдельности.
Но ведут они себя так, как будто знают друг о друге. Когда пользователь изменяет информацию в таблице, все изменения немедленно отражаются на диаграмме, и наоборот.
window
window
window
x y
z a
b c
60 30 10 50 30 20 80 10 10
a b c a
c b
a=50%
b=30%
c=20%
Наблюдатели
Cубъект
Уведомление об изменении
Запросы, модификации
При таком поведении подразумевается, что и электронная таблица, и диа- грамма зависят от данных объекта и поэтому должны уведомляться о лю-
Паттерн
Observer (наблюдатель)
341бых изменениях в его состоянии. И нет никаких причин, ограничивающих количество зависимых объектов; для работы с одними и теми же данными может существовать любое число пользовательских интерфейсов.
Паттерн наблюдатель описывает, как устанавливаются такие отношения.
Ключевыми объектами в нем являются субъект и наблюдатель. У субъекта может быть сколько угодно зависимых от него наблюдателей. Все на- блюдатели уведомляются об изменениях в состоянии субъекта.
Получив уведомление, наблюдатель опрашивает субъекта, чтобы синхронизировать с ним свое состояние.
Такого рода взаимодействие часто называется отношением издатель — подпис- чик. Субъект издает или публикует уведомления и рассылает их, даже не имея информации о том, какие объекты являются подписчиками. На получение уведомлений может подписаться неограниченное количество наблюдателей.
Применимость
Основные условия для применения паттерна наблюдатель:
у абстракции есть два аспекта, один из которых зависит от другого. Ин- капсуляции этих аспектов в разные объекты позволяют изменять и по- вторно использовать их независимо;
при модификации одного объекта требуется изменить другие, и вы не знаете, сколько именно объектов нужно изменить;
один объект должен оповещать других, не делая предположений об уве- домляемых объектах. Другими словами, объекты не должны быть тесно связаны между собой.
Структура
GetState()
SetState()
subjectState
ConcreteSubjectAttach(Observer)
Detach(Observer)
Notify()
SubjectUpdate()ObserverUpdate()
observerState observers subject
ConcreteObserverreturn subjectState
Для всех наблюдателей о {
o Update()
}
observerState =
subject–>GetState()
>
342 Глава 5. Паттерны поведения
Участники
Subject — субъект:
• располагает информацией о своих наблюдателях. За субъектом может
«следить» любое число наблюдателей;
• предоставляет интерфейс для присоединения и отделения наблюда- телей;
Observer — наблюдатель:
• определяет интерфейс обновления для объектов, которые должны уведомляться об изменении субъекта;
ConcreteSubject — конкретный субъект:
• сохраняет состояние, представляющее интерес для конкретного на- блюдателя
ConcreteObserver
;
• посылает информацию своим наблюдателям, когда происходит из- менение;
ConcreteObserver — конкретный наблюдатель:
• хранит ссылку на объект класса
ConcreteSubject
;
• сохраняет данные, которые должны быть согласованы с данными субъекта;
•
реализует интерфейс обновления, определенный в классе
Observer
, чтобы поддерживать согласованность с субъектом.
Отношения
объект
ConcreteSubject уведомляет своих наблюдателей о любом из- менении, которое могло бы привести к рассогласованности состояний наблюдателя и субъекта;
после получения от конкретного субъекта уведомления об изменении объект
ConcreteObserver может запросить у субъекта дополнительную информацию, которую использует для того, чтобы оказаться в состоя- нии, согласованном с состоянием субъекта.
На схеме взаимодействий показаны отношения между субъектом и двумя наблюдателями.
Паттерн Observer (наблюдатель)
343aConcreteSubject
SetState()
Notify()
Update()
Update()
GetState()
GetState()
aConcreteObserver anotherConcreteObserver
Результаты
Паттерн наблюдатель позволяет изменять субъекты и наблюдатели неза- висимо друг от друга. Субъекты разрешается повторно использовать без участия наблюдателей, и наоборот. Это дает возможность добавлять новых наблюдателей без модификации субъекта или других наблюдателей.
Основные достоинства и недостатки паттерна наблюдатель:
абстрактная связанность субъекта и наблюдателя. Субъект имеет ин- формацию лишь о том, что у него есть ряд наблюдателей, каждый из ко- торых подчиняется простому интерфейсу абстрактного класса
Observer
Субъекту неизвестны конкретные классы наблюдателей. Таким образом, связи между субъектами и наблюдателями носят абстрактный характер и сведены к минимуму.
Поскольку субъект и наблюдатель не являются тесно связанными, они могут находиться на разных уровнях абстракции системы. Субъект бо- лее низкого уровня может уведомлять наблюдателей, находящихся на верхних уровнях, не нарушая иерархии системы. Если бы субъект и на- блюдатель представляли собой единое целое, то получающийся объект либо пересекал бы границы уровней (нарушая принцип их формирова- ния), либо должен был находиться на каком-то одном уровне (нарушая абстракцию уровня);
поддержка широковещательных коммуникаций. В отличие от обычного запроса, для уведомления, посылаемого субъектом, не нужно задавать определенного получателя. Уведомление автоматически поступает всем подписавшимся на него объектам. Субъекта не интересует,
сколько су- ществует таких объектов; от него требуется всего лишь уведомить сво-
344 Глава 5. Паттерны поведения их наблюдателей. Таким образом, мы можем в любое время добавлять и удалять наблюдателей. Наблюдатель сам решает, обработать получен- ное уведомление или игнорировать его;
неожиданные обновления. Поскольку наблюдатели не располагают ин- формацией друг о друге, им неизвестно и о том, во что обходится изме- нение субъекта. Безобидная на первый взгляд операция над субъектом может вызвать целый ряд обновлений наблюдателей и зависящих от них объектов. Более того, нечетко определенные или плохо поддержи- ваемые критерии зависимости могут стать причиной непредвиденных обновлений, отследить которые очень сложно.
Проблема усугубляется еще и тем, что простой протокол обновления не содержит никаких сведений о том, что
именно изменилось в субъекте. Без дополнительного протокола, который позволяет получить информацию об изменениях, наблюдатели будут вынуждены проделать сложную ра- боту для косвенного получения такой информации.
Реализация
В этом разделе обсуждаются вопросы, относящиеся к реализации механизма зависимостей:
связывание субъектов с наблюдателями. Этим простейшим способом субъект может отслеживать всех наблюдателей, которым он должен по- сылать уведомления — то есть хранить на них явные ссылки. Однако при большом числе субъектов при нескольких наблюдателях это может привести к слишком высоким затратам. Один из возможных компро- миссов — экономия памяти за счет времени с использованием ассоциа- тивного массива (например, хеш-таблицы) для хранения отображения между субъектами и наблюдателями. Тогда субъект, у которого нет на- блюдателей, не будет зря расходовать память. С другой стороны, при таком подходе увеличивается время поиска наблюдателей;
наблюдение более чем за одним субъектом. Иногда наблюдатель может зависеть более чем от одного субъекта. Например, у электронной табли- цы бывает более одного источника данных. В таких случаях необходи- мо расширить интерфейс
Update
, чтобы наблюдатель мог узнать, какой субъект прислал уведомление. Субъект может просто передать себя в параметре операции
Update
, тем самым сообщая наблюдателю, что
именно нужно обследовать;
кто инициирует обновление? Для сохранения согласованности субъ- ект и его наблюдатели полагаются на механизм уведомлений. Но какой
Паттерн Observer (наблюдатель)
345
именно объект вызывает операцию
Notify для инициирования обновле- ния? Возможны два варианта:
• операции класса
Subject
, изменившие состояние, вызывают
Notify для уведомления об этом изменении. Преимущество такого подхо- да в том, что клиентам не надо помнить о необходимости вызывать операцию
Notify субъекта. Недостаток же заключается в том, что при выполнении каждой из нескольких последовательных операций будут проводиться обновления, что может привести к неэффективной работе программы;
• ответственность за своевременный вызов
Notify возлагается на клиен- та. Преимущество: клиент может отложить инициирование обновле- ния до завершения серии изменений, исключив тем самым ненужные промежуточные обновления. Недостаток: у клиентов появляется дополнительная обязанность. Это увеличивает вероятность ошибок, поскольку клиент может забыть вызвать
Notify
;
висячие ссылки на удаленных субъектов. Удаление субъекта не должно приводить к появлению висячих ссылок у наблюдателей. Избежать это- го можно, например, поручив субъекту уведомлять всех своих наблюда- телей о своем удалении, чтобы они могли уничтожить хранимые у себя ссылки. В общем случае простое удаление наблюдателей не годится, так как на них могут ссылаться другие объекты, и под их наблюдением мо- гут находиться другие субъекты;
гарантии целостности состояния субъекта перед отправкой уведом-
ления. Важно быть уверенным, что перед вызовом операции
Notify со- стояние субъекта непротиворечиво, поскольку в процессе обновления собственного состояния наблюдатели будут опрашивать состояние субъекта.
Правило непротиворечивости легко случайно нарушить, если операции одного из подклассов класса
Subject вызывают унаследованные опера- ции. Например, в следующем фрагменте уведомление отправляется, когда состояние субъекта противоречиво:
void MySubject::Operation (int newValue) {
BaseClassSubject::Operation(newValue);
// Отправить уведомление
_myInstVar += newValue;
// Обновить состояние подкласса (слишком поздно!)
}