Система объектов, управляемых сообщениями
Здесь мы рассмотрим очень приближенную модель того, что реально происходит при программировании в объектно-ориентированных библиотеках, основанных на обработке сообщений (или событий) объектами программы. (Например, OWL в Windows или Turbo Vision в DOS) . Система объектов, управляемых сообщениями, должна включать в себя несколько классов, взаимодействие между которыми скрыто от внешнего наблюдателя :
-класс сообщений ;
-базовый класс объектов, управляемых сообщениями ;
-класс прикладной программы, единственный объект которого является аналогом main и реализует в своем методе run диспетчер сообщений и объектов.
Прежде всего, вводится понятие сообщения - как единственной и универсальной единицы обмена данными между объектами. Сообщение не является адресным, поскольку объекты не располагают информацией ни своем количестве, ни о своем расположении. Вместо этого в сообщение вводится код или вид сообщения. Кроме того, сообщение в зависимости от кода может нести данные и указатель на область памяти (например, объект может передать указатель на самого себя).
// Класс сообщений -----------------------------------
#define ms_NULL 0 // Пустое сообщение
#define ms_KEYB 1 // Символ клавиатуры
#define ms_TICK 2 // Тик таймера
#define ms_MS_MOVE 3 // Перемещение мыши
#define ms_MS_CLICK 4 // Кнопка мыши
#define ms_ECHO 5 // Ответ объекта с this
#define ms_EXIT 6 // Завершение программы
#define ms_KILL 7 // Уничтожение объекта
#define ms_BORN 8 // Порождение объекта
class MS
{
public:
char code; // Код сообщения
int x,y; // Параметры сообщения
void *p; // Указатель на данные
MS(char,int,int,void *); // Констуктор - создать сообщение
~MS();
void clear()
{ code=ms_NULL; }; // " Очистить" сообщение
};
MS::MS(char c,int xx,int yy,void *q)
{ code = c; x = xx; y = yy; p = q; }
MS::~MS() {}
Все взаимодействующие элементы программы должны быть объектами, производными от одного общего класса - класса объектов, управляемых сообщениями.
zlist messages; // Сообщения программы
public:
PRG();
~PRG();
void SendEvent(char,int,int,void *);
void run();
};
PRG *OBJ::programm = NULL;
Конструктор связывает объект-программу с объектами класса OBJ путем установки в них статического указателя на самого себя. После этого все объекты могут передавать сообщения методом SendEvent , который просто ретранслируется в аналогичный метод SendEvent в классе PRG. Последний создает объект-сообщение и помещает указатель на него в конец списка сообщений messages .
// Конструктор: связаться с классом OBJ
PRG::PRG()
{ OBJ::programm = this; }
//---- Прием и запись нового сообщения -------------------
void PRG::SendEvent(char code0,int x0,int y0, void *p0)
{ MS *pm;
pm = new MS(code0,x0,y0,p0);
messages((void *)pm); // Переопределенная операция x(void*) -
} // включить последним
Метод run представляет собой диспетчер сообщений, обеспечивая посредством их связь типа " каждый с каждым" . Это значит, что каждое сообщение пропускается через всю цепочку объектов, которые либо игнорируют его, либо обрабатывают. Обработка может закончится очисткой сообщения, тогда оно будет принято всего одним (первым) объектом, В противном случае сообщение будет широковещательным, то есть на него будут реагировать все объекты, которые настроены на его обработку.
// Диспетчер сообщений и объектов -------------------
void PRG::run()
{ MS *pm;
clock_t t;
t = clock();
while(1)
{
for (n=0; (pm = (MS *)messages.Remove(0) !=NULL; n++)
{ // Пока есть сообщения в очереди -
switch (pm->code) // исключить первое
{ // и переключиться по коду
case ms_BORN: // Служебное сообщение от конструктора
objects(pm->p); // объекта - включить в список объектов
break;
case ms_NULL:
break; // Пустое сообщение
case ms_EXIT:
return; // Сообщение о завершении работы
case ms_KILL: // Сообщение об уничтожении процесса -
void *q = pm->p; // Посылается объектом, который хочет
// завершить работу
pp = (OBJ *)objects.Remove(q);
if (pp == NULL) break; // Исключить его из списка
delete pp; // Разрушить динамический объект -
break; // вызвать виртуальный деструктор
default: // Остальные сообщения передаются
int n=objects.size(); // всем объектам в списке
for (m=0; m<n; m++)
{ // Извлечь указатель на объект по номеру
OBJ *pp = (OBJ*)objects[m];
// Вызвать виртуальную функцию обработки
// сообщение объектом
pp->HandleEvent(pm);
// Сообщение обработано данным объектом
if (pm->code == ms_NULL) break;
}
}
delete pm; // Уничтожить сообщение
}
}
//----- Проверка источников сообщений ---------------------
if (kbhit()) // Сообщение от клавиатуры
{
pm = new MS(ms_KEYB,(int)getch(),0,NULL);
messages((void *)pm);
}
if ( clock()-t > 1) // Сообщение от таймера (часы)
{ t = clock();
pm = new MS(ms_TICK,0,0,NULL);
messages((void *)pm);
}
}}
Диспетчер состоит из двух частей. Первая часть обеспечивает систему взаимосвязи объектов через сообщения. Для этого она извлекает по одному сообщению из списка и " пропускает" его через все объекты. Обработка сообщения осуществляется виртуальной функцией HandleEvent , конкретный вид которой будет зависеть от того, к какому производному классу относится этот объект. " Пропускание" сообщения может прерваться, если объект его сбрасывает. Так или иначе, по окончании этого процесса сообщение уничтожается.
Среди сообщений следует выделить служебные, касающиеся создания и уничтожения объектов. Когда в программе создается новый объект (а создается он в процессе обработки сообщения другим объектом), то его конструктор посылает сообщение о своем " рождении" , по которому указатель на этот объект включается в общий список. Уничтожение объекта происходит сложнее. Дело в том, что в процессе обработки сообщения объект не может уничтожить сам себя (то есть выполнить что-то вроде delete this ). Хотя бы потому, что диспетчер продолжает процесс обработки сообщений. Вместо этого объект посылает служебное сообщение с просьбой " убить его" , которое отрабатывается диспетчером.
Диспетчер исключает динамический объект из списка и уничтожает его (как динамический объект).
Вторая часть диспетчера опрашивает внешние источники сообщений - клавиатуру и часы - и формирует сообщения от них, которые включает в очередь диспетчера.
Деструктор " вычищает" очереди сообщений и объектов, разрушая сами сообщения и объекты.
// Деструктор: уничтожение сообщений и объектов ----- --------
PRG::~PRG()
{
while ((MS *p=messages.Remove(0))!=NULL) delete p;
while ((OBJ *q=objects.Remove(0)) !=NULL) delete q;
}
Для создания прикладной программы, нужно определить производный класс от класса PRG , конструктор которого порождает те объекты, которые изначально присутствуют в программе. Естественно, что они должны быть из классов, управляемых сообщениями.
// Производный класс прикладной программы --------------------
class myPRG : public PRG
{
public:
myPRG() { MOUSE *pms = new MOUSE; }
};
Основная программа main состоит из двух строчек - создать единственный объект класса myprog и вызвать для него метод run , поскольку все, что можно было, уже реализовано в предыдущих классах.
void main()
{
myPRG myprog;
myprog.run();
}
В заключение рассмотрим, как выглядит процесс программирования, основанный на обработке событий. Его сущность заключается в разработке системы объектов, системы сообщений и реакций на них со стороны объектов - своего рода " правил игры" . Конкретная конфигурация объектов и их связей в программе не оговаривается, она возникает уже в процессе взаимодействия объектов по разработанным правилам. Поэтому такая систем особенно удобна там, где количество объектов заранее не известно или меняется по ходу программы. Рассмотрим в качестве примера несколько классов движущихся по экрану объектов с простыми правилами взаимодействия. Для начала определим классы - видимая и движущаяся точки и курсор, управляемый с клавиатуры.
Класс " видимая точка" представляет собой символ, отображаемый на экране в заданной позиции. Данные этого объекта включают в себя координаты точки и код символа, а методы - перемещение объекта на одну позицию и отображение.
Поскольку данный класс является промежуточным, то его объекты на сообщения никак не реагируют.
// Класс "Видимая точка" ---------------------------
class PT : public OBJ
{ char sym; // Отображаемый символ
int x,y; // Координаты точки
public:
PT(char); // Конструктор
~PT(); // Деструктор
void on(); // Высветить
void off(); // Погасить
void up(); // Перемещения точки
void down();
void left();
void right();
void OnPlace(int,int); // Позиционирование точки
void HandleEvent(MS *); // Обработка сообщений
};
//--------------------------------------------------------------
PT::PT(char c) { sym = c; x = 0; y = 0; }
void PT::off() { gotoxy(x+1,y+1); putch(' '); }
void PT::on() { gotoxy(x+1,y+1); putch(sym); }
void PT::OnPlace(int xx, int yy) {off(); x = xx; y = yy; on(); }
void PT::left() { off(); if (x > 0) x--; on(); }
void PT::right() { off(); if (x < 79) x++; on(); }
void PT::up() { off(); if (y > 0) y--; on(); }
void PT::down() { off(); if (y < 24) y++; on(); }
PT::~PT() {off(); }
void PT::HandleEvent(MS *pm) {}
Курсор - это " видимая точка" , которая управляется с клавиатуры, поэтому этот объект выделяет сообщения от клавиатуры с соответствующими кодами и вызывает для них методы перемещения из класса " видимой точки" .
// Класс " Курсор - управляемый с клавиатуры"
class MOUSE : public PT
{ public:
void HandleEvent(MS *);
MOUSE();
~MOUSE();
};
//--------------------------------------------------------------
MOUSE::MOUSE() : PT('#') // Конструктор - сконструировать
{ OnPlace(40,12); } // точку " #" и установить ее на 40,12
void MOUSE::HandleEvent(MS *pm)
{
switch (pm->code)
{ // Реакция курсора на сообщения от клавиатуры
case ms_KEYB:
switch(pm->x)
{
case '8': up(); pm->clear(); break;
case '2': down(); pm->clear(); break;
case '4': left(); pm->clear(); break;
case '6': down(); pm->clear(); break;
case '0': pm->clear(); SendEvent(ms_KILL,0,0,(void *)(OBJ *)this); break;
case '5': pm->clear(); SendEvent(ms_EXIT,0,0,NULL); break;
}
break;
}
}
MOUSE::~MOUSE() {}
Класс " движущаяся точка" имеет дополнительные элементы данных в объекте - составляющие скорости перемещения объекта по координатам. Такой объект должен реагировать на сообщения от таймера - периодически менять свое положение на экране через определенное количество " тиков" - сообщений от таймера, пропорционально составляющим его скорости.
// Класс "Движущиеся точки" -------------------------
class MOVING : public PT
{ int dx,dy;
public:
void oneStep();
void HandleEvent(MS *);
MOVING(int,int,int,int);
~MOVING();
};
В принципе - идея уже достаточно ясна. Единственный вопрос возникает далее вокруг взаимодействия объектов - например, в игровой программе необходимо будет отслеживать столкновение движущихся точек или курсора. Поскольку объекты " не знают о существовании друг друга" и, если не предусматривать другой системы ограниченного взаимодействия объектов, то единственным способом является периодическая посылка объектами сообщений со своими координатами. Объект, получивший такое сообщение, сравнивает свои координаты с полученными и соответственно определяет наличие столкновения.