Документ 5097359

реклама
Паттерны поведения
 Паттерны поведения связаны с алгоритмами и
распределением обязанностей между объектами.
 Речь в них идет не только о самих объектах и классах,
но и о типичных способах взаимодействия
 Паттерны поведения характеризуют сложный поток
управления, который трудно проследить во время
выполнения программы

Внимание акцентировано не на потоке управления как
таковом, а на связях между объектами
Паттерны поведения











Цепочка обязанностей (Chain of Responsibility)
Команда (Command)
Интерпретатор (Interpreter)
Итератор (Iterator)
Посредник (Mediator)
Хранитель (Memento)
Наблюдатель (Observer)
Состояние (State)
Стратегия (Strategy)
Шаблонный метод (Template Method)
Посетитель (Visitor)
Паттерн «Команда»
 Инкапсулирует запрос как объект, позволяя тем
самым:
 Задавать параметры клиентов для обработки
соответствующих запросов
 Ставить запросы в очередь или протоколировать их
 Поддерживать отмену операций
 Альтернативные названия – Action, Transaction
Применимость
 Параметризация объектов выполняемым действием
 Реализация пунктов меню
 Определение, постановка в очередь и выполнение
запросов в разное время
 Возможность выполнения команды в другом процессе
 Поддержка отмены операций
 Undo/Redo
 Протоколирование изменений
 Восстановление состояния после сбоя
 Структурирование системы на основе
высокоуровневых операций, составленных из
примитивных (транзакции)
Объявляет интерфейс для
выполнения операции
Структура
Client
Command
Invoker
Execute()
Инициатор.
Обращается к команде
для выполнения запроса
Receiver
Action()
receiver
•Определяет связь между
объектом-получателем Receiver и
действием
•Реализует операцию Execute
путем вызова соответствующих
операций получателя
ConcreteCommand
Execute()
receiver->Action();
Состояние
Создает объект класса
ConcreteCommand и
устанавливает его получателя
•Располагает информацией о способах выполнения
операций, необходимых для удовлетворения
запроса
•В роли получателя может выступать любой класс.
Отношения
 Клиент создает объект ConcreteCommand и
устанавливает для него получателя
 Инициатор Invoker сохраняет объект
СoncreteCommand
 Инициатор отправляет запрос, вызывая операцию
команды Execute
 Если поддерживается отмена выполненных действий, то
ConcreteCommand перед вызовом Execute сохраняет
информацию о состоянии, достаточную для выполнения
отката
 Объект ConcreteCommand вызывает операции
получателя для выполнения запроса
Диаграмма взаимодействий
receiver
client
command
invoker
new Command(receiver)
Action()
Execute()
Результаты
 Команда разрывает связь между объектом,
инициирующим операцию, и объектом, имеющим
информацию о том, как ее выполнить
 Команды - это самые настоящие объекты
 Допускается манипулировать ими и расширять их точно
так же, как в случае с любыми другими объектами
 Из простых команд можно собирать составные
 В общем случае составные команды описываются
паттерном Компоновщик
 Добавлять новые команды легко, поскольку никакие
существующие классы изменять не нужно
Поддержка отмены и повтора
операций
 В классе ConcreteCommand должна сохраняться информация
для отмены операции
 Объект-получатель
 Аргументы операции, выполненное пользователем
 Исходные значения различных атрибутов получателя, которые
могли измениться в результате обработки запроса

Получатель должен предоставлять операции, позволяющие команде
вернуть его в исходное состояние
 Для нескольких уровней отмены необходимо вести историю
выполненных команд
 Отмена операций осуществляется в обратном порядке, повтор
– в прямом
 Объекты команд, возможно, потребуется скопировать перед
помещением в список истории команд

Используется паттерн «Прототип»
Пример использования
 В текстовом редакторе предоставить возможность
отмены и повтора операций по редактированию
текста
 Вставка текста
 Удаление текста
 Количество уровней отмены команд
ограничивается лишь объемом доступной памяти
Управляет созданием
команд и передачей их
Invoker-у
Предоставляет операции
Undo/Redo
Иерархия классов
Объявляет
операции для
выполнения и
отмены команд
Обеспечивает
согласованное
выполнение и
отмену
команды
CCommand
CCommands
CCommandImpl
CInsertTextCommand
CEraseTextCommand
CDocument
Invoker
Обеспечивает «историю
изменений»
Предоставляет операции
Undo/Redo
CTextEditor
Receiver
Обеспечивает основные
операции над документом
Конкретные команды
редактирования
документа
CDocument – получатель команд
class CDocument
{
public:
std::string const& GetText()const{return m_text;}
void InsertText(std::string const& text, size_t pos = std::string::npos)
{
if (pos == std::string::npos) pos = m_text.length();
m_text.insert(pos, text);
}
void RemoveText(size_t pos, size_t length)
{
m_text.erase(pos, length);
}
private:
std::string m_text;
};
std::ostream & operator<<(std::ostream & strm, CDocument const& doc)
{
return strm << doc.GetText() << "\n";
}
CCommand и CCommandImpl
class CCommand
{
public:
virtual ~CCommand(){}
virtual void Execute() = 0;
virtual void Unexecute() = 0;
};
typedef boost::shared_ptr
<CCommand> CCommandPtr;
Данные методы
реализовываются
конкретными
командами
class CCommandImpl : public CCommand
{
public:
CCommandImpl(bool executed = false):m_executed(executed){ }
virtual void Execute()
{
if (m_executed)
throw std::logic_error("The command has been already executed");
DoExecute(); m_executed = true;
}
virtual void Unexecute()
{
if (!m_executed)
throw std::logic_error("The command has not been executed yet");
DoUnexecute(); m_executed = false;
}
protected:
virtual void DoExecute()=0;
virtual void DoUnexecute()=0;
private:
bool m_executed;
};
Команда вставки текста
class CInsertTextCommand : public CCommandImpl
{
public:
CInsertTextCommand(CDocument & doc, std::string const& text, size_t pos = std::string::npos)
:m_doc(doc),m_text(text)
,m_pos(pos == std::string::npos ? doc.GetText().length() : pos)
{
}
При выполнении команды
protected:
происходит вставка текста в
virtual void DoExecute()
документ
{
m_doc.InsertText(m_text, m_pos);
}
При отмене команды
происходит удаление
virtual void DoUnexecute()
вставленного фрагмент из
{
документа
m_doc.RemoveText(m_pos, m_text.length());
}
private:
CDocument& m_doc;
std::string m_text;
size_t
m_pos;
};
Команда стирания текста
class CEraseTextCommand : public CCommandImpl
{
public:
CEraseTextCommand(CDocument & doc, size_t pos, size_t length = std::string::npos)
:m_doc(doc),m_pos(pos),m_length(length)
{
}
При выполнении команды
protected:
происходит сохранение
virtual void DoExecute()
стираемого текста в поле
{
m_text команды
m_text = m_doc.GetText().substr(m_pos, m_length);
m_doc.RemoveText(m_pos, m_length);
При отмене команды
}
происходит восстановление
virtual void DoUnexecute()
удаленного текста из поля
{
m_text
m_doc.InsertText(m_text, m_pos);
m_text.clear();
}
private:
CDocument& m_doc;
std::string m_text;
size_t
m_pos, m_length;
};
Класс CCommands (начало)
class CCommands
{
private:
std::vector<CCommandPtr> m_commands;
size_t m_currentCommand;
public:
CCommands()
:m_currentCommand(0)
{
}
bool UndoAvailable()const
{
return m_currentCommand != 0;
}
bool RedoAvailable()const
{
return m_currentCommand < m_commands.size();
}
...
Проверка доступности
операции Undo()
Проверка доступности
операции Redo
Класс CCommands (окончание)
void AddAndExecute(CCommandPtr pCommand)
{
m_commands.reserve(m_currentCommand + 1);
pCommand->Execute();
if (RedoAvailable()) m_commands.resize(m_currentCommand);
m_commands.push_back(pCommand);
++m_currentCommand;
}
void Undo()
{
if (!UndoAvailable()) throw std::logic_error("Undo is not available");
size_t commandIndex = m_currentCommand - 1;
m_commands[commandIndex]->Unexecute();
m_currentCommand = commandIndex;
}
void Redo()
{
if (!RedoAvailable()) throw std::logic_error("Redo is not available");
m_commands[m_currentCommand]->Execute();
++m_currentCommand;
}
}
Добавляем команду в конец
очереди команд, при
необходимости стирая
«будущие» команды
Отменяем текущую команду,
сдвигаем указатель команд на
одну команду назад
Повторяем отмененную ранее
команду, сдвигаем указатель
команд на одну команду
вперед
Редактор текста (Client)
class CTextEditor
{
public:
CTextEditor(CDocument & doc):m_doc(doc){}
void InsertText(std::string const& text, size_t pos = std::string::npos)
{
m_commands.AddAndExecute(CCommandPtr(new CInsertTextCommand(m_doc, text, pos)));
}
void EraseText(size_t pos, size_t length = std::string::npos)
{
m_commands.AddAndExecute(CCommandPtr(new CEraseTextCommand(m_doc, pos, length)));
}
void Undo()
{
if (m_commands.UndoAvailable()) m_commands.Undo();
}
void Redo()
{
if (m_commands.RedoAvailable()) m_commands.Redo();
}
private:
CDocument & m_doc;
CCommands m_commands;
};
Пример использования
int main(int argc, char* argv[])
{
CDocument doc;
CTextEditor editor(doc);
editor.InsertText("Hello, ");
std::cout << doc;
editor.InsertText("World!");
std::cout << doc;
editor.Undo();
std::cout << doc;
editor.InsertText("world! :)");
std::cout << doc;
editor.EraseText(0, 5);
std::cout << doc;
editor.InsertText("Good bye", 0);
std::cout << doc;
editor.Undo();
std::cout << doc;
editor.Undo();
std::cout << doc;
return 0;
}
Output:
Hello,
Hello, World!
Hello,
Hello, world! :)
, world! :)
Good bye, world! :)
, world! :)
Hello, world! :)
InsertText(«Hello, »)
InsertText(«World!»)
InsertText(«world! »)
EraseText(«Hello»)
InsertText(«Good bye»)
Паттерн «Посетитель»
 Описывает операцию, выполняемую над каждым
объектом из некоторой структуры
 Паттерн позволяет определить новую операцию, не
изменяя классы этих объектов
Применимость
 В структуре присутствуют объекты многих классов
с различными интерфейсами, над которыми
хотелось бы выполнять операции, зависящие от
конкретных классов
 Над объектами, входящими в состав структуры,
надо выполнять разнообразные, не связанные
между собой операции, при этом не хотелось бы
«засорять» классы такими операциями
 Посетитель позволяет объединить родственные
операции, поместив их в отдельный класс
Применимость
 Классы, устанавливающие структуру объектов,
изменяются редко, но новые операции над этой
структурой добавляются часто
 При изменении классов, представленных в
структуре, нужно будет переопределить интерфейсы
всех посетителей, а это может вызвать затруднения
 Поэтому если классы меняются достаточно часто,
лучше определить операции прямо в них
Структура
Объявляет операцию Visit для
каждого класса ConcreteElement в
структуре объектов
Client
•Может перечислить свои
элементы
•Может предоставить
посетителю интерфейс для
посещения своих элементов
•Может быть как составным
объектом, так и коллекцией
ObjectStructure
Visitor
VisitConcreteElementA()
VisitConcreteElementB()
ConcreteVisitor1
VisitConcreteElementA()
VisitConcreteElementB()
Element
Accept(Visitor)
ConcreteElementA
Accept(Visitor v)
OperationA()
v->VisitConcreteElementA(this)
Реализует все операции,
объявленные в классе
Visitor для
соответствующих классов
объектов структуры
Может аккумулировать
результаты
ConcreteVisitor2
VisitConcreteElementA()
VisitConcreteElementB()
Определяет операцию Accept,
принимающую посетителя в
качестве аргумента
ConcreteElementB
Accept(Visitor v)
OperationB()
Реализует операцию
Accept, принимающую
посетителя в качестве
аргумента
v->VisitConcreteElementB(this)
Отношения
 Клиент, использующий паттерн «Посетитель»
должен создать объект класса ConcreteVisitor, а
затем обойти всю структуру, посетив каждый ее
элемент
 При посещении элемента, последний вызывает
операцию посетителя, соответствующую своему
классу, и передает себя в качестве аргумента
Диаграмма взаимодействий
objectStructure
Accept(visitor)
concreteElementA
concreteElementB
concreteVisitor
VisitConcreteElementA(concreteElementA)
OperationA()
Accept(visitor)
VisitConcreteElementB(concreteElementB)
OperationB()
Пример использования
 Есть иерархия геометрических фигур
 Есть коллекция фигур, хранящая указатели на базовый
класс иерархии
 Необходимо вывести информацию обо всех фигурах
коллекции в поток вывода
 Предусмотреть возможность легкой смены формата
вывода информации



Rectangle(20x30)
<rectangle width=“20” height=“30” />
Бинарный формат
 Предусмотреть возможность вывода информации о
фигуре в поток с использованием оператора <<
Иерархия фигур
CShape
CRectangle
CCircle
Проблема
 Оператор вывода в поток должен быть реализован
вне иерархии классов фигур
 С другой стороны он должен обладать
«виртуальным поведением»
 CShape * pShape = new CRectangle(20, 30);
std::cout << *pShape;
 В данном случае должен быть вызван оператор
вывода, специфичный для класса CRectangle, а не
CShape
Решение 1 – сериализация
объекта в строку
 Добавить в класс CShape виртуальный метод
ToString(), возвращающий информацию о фигуре в
виде строки
 Недостатки
 Для поддержки разных форматов вывода придется
добавлять новые операции в иерархию фигур
 В конкретном приложении большая часть таких
операций будет просто не востребована
template <class T>
std::basic_ostream<T>& operator<<(std::basic_ostream<T>& strm, CShape const& shape)
{
strm << shape.ToString();
return strm;
}
Решение 2 – использование
паттерна «Посетитель»
class CShapeVisitor;
class CShape
{
public:
virtual void Accept(CShapeVisitor & v)const = 0;
virtual ~CShape(){}
};
class CRectangle;
class CCircle;
class CShapeVisitor
{
public:
virtual void Visit(CRectangle const& rectangle) = 0;
virtual void Visit(CCircle const& circle) = 0;
};
class CRectangle : public CShape
{
public:
CRectangle(float w, float h)
:m_width(w), m_height(h){}
float GetWidth()const{return m_width;}
float GetHeight()const{return m_height;}
virtual void Accept(CShapeVisitor & visitor)const
{visitor.Visit(*this);}
private:
float m_width, m_height;
};
class CCircle : public CShape
{
public:
CCircle(float r):m_radius(r){}
float GetRadius()const{return m_radius;}
virtual void Accept(CShapeVisitor & visitor)const
{visitor.Visit(*this);}
private:
float m_radius;
};
Посетитель для потокового
вывода в basic_ostream
template <class T>
class COStreamShapeVisitor : public CShapeVisitor
{
public:
COStreamShapeVisitor(std::basic_ostream<T> & strm)
:m_stream(strm)
{
}
void Visit(CRectangle const& rectangle)
{
m_stream << "Rectangle(" << rectangle.GetWidth();
m_stream << 'x' << rectangle.GetHeight() << ')';
}
void Visit(CCircle const& circle)
{
m_stream << "Circle(" << circle.GetRadius() << ')';
}
private:
std::basic_ostream<T> & m_stream;
};
Пример использования
template <class T>
std::basic_ostream<T> & operator <<(std::basic_ostream<T> & strm, CShape const& shape)
{
COStreamShapeVisitor<T> visitor(strm);
shape.Accept(visitor);
return strm;
}
typedef boost::shared_ptr<CShape> CShapePtr;
template <class T>
void PrintShapes(
std::basic_ostream<T> & strm,
std::vector<CShapePtr> const& shapes
)
{
for (size_t i = 0; i < shapes.size(); ++i)
{
strm << *shapes[i] << "\n";
}
}
int main(int argc, char* argv[])
{
std::vector<CShapePtr> shapes;
shapes.push_back(CShapePtr(new CRectangle(20, 30)));
shapes.push_back(CShapePtr(new CCircle(50)));
shapes.push_back(CShapePtr(new CCircle(25)));
PrintShapes(std::cout, shapes);
return 0;
}
Output:
Rectangle(20x30)
Circle(50)
Circle(25)
Достоинства паттерна
«Посетитель»
 Упрощается добавление новых операций
 Достаточно ввести нового посетителя
 Объединение родственных операций
 Несвязанные функции распределяются по отдельным
подклассам класса Visitor
 Аккумулирование состояния
 Результат посещения различных элементов может
аккумулироваться посетителем

В случае использования виртуальных методов пришлось бы
передавать доп. параметр с состоянием
 Посещение различных иерархий классов
 Посещаемые классы не обязательно должны иметь
общего предка
Недостатки паттерна
«Посетитель»
 Затрудняется добавление новых классов
ConcreteElement
 При добавлени конкретного элемента требуется
добавить соответствующую операцию Visit() всем
существующим конкретным посетителям
 Нарушение инкапсуляции
 В отличие от виртуальных функций, посетитель не
может обращаться к приватным и защищенным
данным и методам элементов

Элементы должны предоставлять открытые операции для
доступа к своему внутреннему состоянию, необходимому
посетителям
Паттерн «Наблюдатель»
 Определяет зависимость типа «один ко многим»
между объектами таким образом, что при
изменении состояния одного объекта все
зависящие от него оповещаются об этом и
автоматически обновляются
 Альтернативные названия:
 Dependents (подчиненные)
 Publish-Subscribe (издатель-подписчик)
Применимость
 У абстракции есть дав аспекта, один из которых
зависит от другого
 Инкапсуляция данных аспектов в разные объекты
позволяет изменять и повторно использовать их
независимо
 При модификации одного объекта требуется
изменить другие, и заранее не известно, сколько
именно объектов нужно изменить
 Один объект должен оповещать других, не делая
предположений об уведомляемых объектах
•Располагает информацией о своих наблюдателях. За
субъектом может «следить» любое число наблюдателей
•Предоставляет интерфейс для присоединения и
отсоединения наблюдателей
Структура
Subject
Attach(Observer)
Detach(Observer)
Notify()
ConcreteSubject
GetState()
SetState()
Определяет интерфейс
обновления для объектов, которые
должны быть уведомлены об
изменениях субъекта
observers
Observer
Update()
for each Observer o {
o->Update()
}
subject
return subjectState
subjectState
•Сохраняет состояние, представляющее
интерес для конкретного наблюдателя
•Посылает уведомление своим
наблюдателям, при своем изменении
ConcreteObserver
Update()
observerState
observerState = subject->GetState()
•Хранит ссылку на объект класса ConcreteSubejct
•Сохраняет данные, которые должны быть
согласованы с данными субъекта
•Реализует интерфейс обновления,
определенный в классе Observer, чтобы
поддерживать согласованность с субъектом
Отношения
 Объект ConcreteSubject уведомляет своих
наблюдателей о любом изменении, которое могло
бы привести к рассогласованности состояний
наблюдателя и субъекта
 После получения от конкретного субъекта
уведомления об изменении объект
ConcreteObserver может запросить у субъекта
дополнительную информацию, которую
использует для того, чтобы оказаться в состоянии,
согласованном с состоянием субъекта
Диаграмма взаимодействий
concreteSubject
concreteObserver
anotherConcreteObserver
SetState()
Notify()
Update()
GetState()
Update()
GetState()
Результаты
 Паттерн «Наблюдатель» позволяет изменять
субъекты и наблюдатели независимо друг от друга
 Это позволяет добавлять новых наблюдателей без
модификации субъекта или других наблюдателей
Достоинства
 Абстрактная связанность субъекта и наблюдателя
 Субъекту неизвестны конкретные классы
наблюдателей

Субъект и наблюдатель могут находиться на разных
уровнях абстракции системы
 Поддержка широковещательных коммуникаций
 Уведомление автоматически поступает всем
подписавшимся объектам


Наблюдатель сам решает, обработать полученное
уведомление или игнорировать его
Наблюдатели могут быть в любое время добавлены или
удалены
Недостатки
 Неожиданные обновления
 Т.к. наблюдатели не располагают информаций друг о
друге, им не известно, во что обходится изменение
субъекта
 Простой протокол обновления не содержит никаких
сведений о том, что именно изменилось в субъекте

Без дополнительных сведений о характере обновлений
наблюдатели вынуждены проделать сложную работу для
его выяснения
Наблюдение более чем за одним
субъектом
 Наблюдатель иногда может зависеть от более чем
одного субъекта
 Окно визуализации (наблюдатель) игрового поля в
стратегических играх зависит от состояния
нескольких боевых единиц (субъектов)
 В этом случае в метод Update передается еще и
ссылка на субъект, приславший уведомление
 Субъект может передать себя в качестве параметра
операции Update
Кто инициирует обновление?
 Субъекты
 Клиентам не надо помнить о необходимости вызывать
операцию Notify субъектов
 Недостаток

при выполнении серии последовательных операций над
объектом будут производиться обновления, что может снизить
эффективность работы программы
 Клиент
 Клиент может отложить инициирование обновления до
завершении серии изменений, исключив ненужные
промежуточные обновления
 Недостаток

У клиентов появляется дополнительная обязанность, т.к.
возможно забыть вызвать Notify()
Висячие ссылки на удаленные
субъекты
 Удаление субъекта не должно приводить к
появлению «висячих ссылок» на него у
наблюдателей
 Решение:
 Субъект должен уведомлять всех своих
наблюдателей о своем удалении, чтобы они могли
уничтожить хранимые у себя ссылки
Гарантии непротиворечивости состояния
субъекта перед отправкой уведомления
 Перед вызовом Notify() состояние субъекта должно
быть непротиворечивым, т.к. состояние будут
опрашивать наблюдатели в процессе своего
обновления
 Решение (Паттерн «Шаблонный метод»)
 Определить примитивную операцию, замещаемую в
подклассах
 Метод Notify() вызвать после вызова такой операции
 Важно всегда фиксировать, какие операции класса
Subject инициируют обновления
Избегайте зависимости протокола
обновления от наблюдателя
 Субъект часто транслирует подписчикам
дополнительную информацию о характере изменения
 Модель «вытягивания» (pull model)
 Субъект посылает минимум информации об изменении,
наблюдатели запрашивают детали позднее
 Недостаток – наблюдателям приходится выяснять без помощи
субъекта, что изменилось
 Модель «проталкивания» (push model)
 Субъект посылает детальную информацию об изменении,
независимо нужно это наблюдателям или нет
 Недостаток – снижение повторного использования кода, т.к.
классы Subject обладают некоторой информацией об Observer,
что не всегда может быть верным
Явное специфицирование
представляющих интерес модификаций
 При регистрации наблюдателя указывается, какие
события его интересуют
 При наступлении события субъект информирует
лишь тех наблюдателей, которые проявили к нему
интерес

При этом субъект передает изменившийся аспект своим
наблюдателям в виде параметра операции Update
Пример использования
 Цифровые и аналоговые часы
 В роли субъекта выступает таймер часов


Предоставляет время (часы, минуты секунды)
Предоставляет метод Tick() для обновления времени
 В роли конкретных наблюдателей выступают классы


CDigitalClock
CAnalogClock
Субъект
class CObserver;
class CSubject
{
public:
virtual void Attach(CObserver * pObserver)
{m_observers.push_back(pObserver); }
virtual void Detach(CObserver * pObserver)
{
CObservers::iterator it = std::find(m_observers.begin(), m_observers.end(), pObserver);
if (it == m_observers.end()) throw std::invalid_argument("Unknown observer");
m_observers.erase(it);
}
virtual void Notify()
{
CObservers::iterator it;
for (it = m_observers.begin(); it != m_observers.end(); ++it)
(*it)->Update(this);
}
private:
typedef std::vector<CObserver*> CObservers;
CObservers m_observers;
};
Конкретный субъект - таймер
class CClockTimer : public CSubject
{
public:
int GetHours();
int GetMinute();
int GetSecond();
void Tick()
{
// изменяем время
// ...
// уведомляем наблюдателей
Notify();
}
};
Наблюдатели
class CSubject;
class CObserver
{
public:
virtual ~CObserver(){}
virtual void Update(CSubject * pSubject) = 0;
};
class CDigitalClock : public CObserver
{
public:
CDigitalClock (CClockTimer * pClockTimer)
:m_pTimer(pClockTimer)
{
}
virtual void Update(CSubject * pSubject)
{
// обновляем время в цифровом виде
}
private:
CClockTimer * m_pTimer;
};
class CAnalogClock : public CObserver
{
public:
CAnalogClock(CClockTimer * pClockTimer)
:m_pTimer(pClockTimer)
{
}
virtual void Update(CSubject * pSubject)
{
// обновляем время в аналоговом виде
}
private:
CClockTimer * m_pTimer;
};
Пример использования
int main(int argc, char* argv[])
{
// субъект
CClockTimer timer;
// наблюдатели
CDigitalClock digitalClock(&timer);
CAnalogClock analogClock(&timer);
// присоединяем наблюдателей
timer.Attach(&digitalClock);
timer.Attach(&analogClock);
// обновляем таймер – обновляется изображение на часах
timer.Tick();
timer.Tick();
return 0;
}
Скачать