Простите, убогого, но немогу кончить. Уже семь дней...
*** ШАГ 5. Получился Медиатор *
На шаге 4 мы придумали не такой уж плохой механизм рассылки и обработки широковещательных сообщений. Я говорю мы, поскольку, на форумах кивали в сторону возможности использования SendMessage для обработки сообщений. При этом вызов методов CMessanger, например, CMessanger::EnableControls, CMessanger::ClearControls удобнее, чем рассылка соответствующих сообщений при помощи «универсального» метода CMessanger::SendMessage. В последнем случае необходимо по условному переходу ещё передать управление конкретному обработчику в зависимости WPARAM wParam, LPARAM lParam.
Однако здесь привлекательна, лишь форма (использование theMsg.EnableControls вместо theMsg.SendMessage(UM_NOTIFY, IDW_ALL, UM_ERASE, 0)). По существу CMessanger::EnableControls всё равно используется метод CMessanger::SendMessage, а тот в свою очередь использует метод рассылки сообщений MFC CWnd-объектам или API, см. врезку кода выше. Да, погнавшись за внешней стороной дела – замоделировались. Как справедливо, заметил один из участников форума, так мы «едем из Москвы в Подмосковье через Колыму». Надо сказать, что это участник форума весьма вежливый человек, и выразился мягко, поскольку едем не только через Колыму, но … как мудрёно! А сколько «технического» кода придётся написать? То, что надо написать строчки кода для каждого из методов ИНТЕРФЕЙСА наших команд в CMessanger (EnableControls, ClearControls и т.д) – это понятно, как бы мы не делали. Неизбежные накладные расходы, если необходим броадкастинг. Но то, что для каждого CWnd-класса придётся ещё и дополнительные макросы писать – последняя капля. Макросы месадж-мапа простые, но их придётся писать для каждого класса CWnd-объектов, которые участвуют в широковещательной рассылке. Ну и сами методы CWnd-объектов. Где уж там до useful programming style... Всё в топку!
Для того, чтобы отказаться от MS-SendMessage, надо сделать так, чтобы объекты, участвующие в оповещениях о событиях приложения, имели соответствующие методы, которые они наследовали бы от некого объекта CMsgItem, уже имеющего соответствующий командный «интерфейс». Для вызова виртуальных методов этого «интерфейса» CMessаnger должен знать адрес vtb CMsgItem, а не указатель на сам CWnd-объект. Расширим определение CWndInfo, добавив CMsgItem* m_pMsgItem, и немного упростим мембер-функции CMessаnger:
///////////////////////////////////////////////////
//
// file “utilwnd.h”
//
///////////////////////////////////////////////////
#include <afxtempl.h>
#define IDW_ALL 0 // send message to all CWnd objects
class CMsgItem; // forward declaration
////////////////////////////////////////////////////
// CMessanger class
class CMessanger {
public:
class CWndInfo { // object info class
public:
void* m_pWnd;// pointer to the object registered
UINT ID; // user symb.const. for the object registered
CMsgItem* m_pMsgItem;
public:
CWndInfo() : m_pWnd(NULL), m_pMsgItem(NULL), ID(0) {};
CWndInfo(void* pWnd, CMsgItem* pMsgItem = NULL, UINT ID_WND = 0)
{
m_pWnd = pWnd; m_pMsgItem = pMsgItem; ID = ID_WND;
}
~CWndInfo() {};
};
public:
CMessanger() : status(0) {};
// register object in CWndInfo array. Save pointer to the object,
// command inteface and user defined symbolic constant
// ID_WND for this object
void Register(void* pWnd, CMsgItem* pMsgItem, UINT ID_WND)
{
m_aWnd.Add(new CWndInfo(pWnd, pMsgItem, ID_WND));
}
// get CWnd* pointer from CWndInfo array by user defined constant ID_WND
CWnd* GetWnd(UINT ID_WND)
{
for (int i = 0; i < m_aWnd.GetSize(); i++)
if ( ID_WND == m_aWnd.GetAt(i)->ID)
return (CWnd*) (m_aWnd.GetAt(i)->m_pWnd);
return NULL;
}
// complete status of message
UINT GetStatus(void) { return status; }
void SetStatus(UINT _status) { status = _status; }
//**** main message handling**** virtual bool FillControls (UINT message, UINT ID_WND = IDW_ALL) {return true;};
virtual bool DisplayControls(UINT message,UINT ID_WND = IDW_ALL) {return true;};
//**** additional message handling**** // broadcast (ID_WND == IDW_ALL) or direct message handling
void SendMessage(UINT message, UINT ID_WND = IDW_ALL,
WPARAM wParam = 0,LPARAM lParam = 0);
// special message handling
void EnableControls(bool Еnable, UINT ID_WND = IDW_ALL);
void ClearControls(UINT ID_WND = IDW_ALL);
private:
CArray <CWndInfo*, CWndInfo*> m_aWnd;
UINT status;
};
extern CMessanger theMsg;
////////////////////////////////////////////////////
// CMsgItem class
class CMsgItem {
public:
void Register(CMessanger* pMsg, void* pWnd, UINT ID_WND)
{
pMsg->Register( pWnd , this, ID_WND);
};
//**** additional message handling**** virtual void SendMessage(UINT message, WPARAM wParamMsg=0, LPARAM lParam=0){}
// special message handling
virtual void EnableControls(bool Enable){}
virtual void ClearControls (void) {}
};
//-----------------------------------------------------
///////////////////////////////////////////////////
//
// file “utilwnd.cpp”
//
///////////////////////////////////////////////////
#include "StdAfx.h"
#include "utilwnd.h"
CMessanger theMsg;
// general broadcast (ID_WND == IDW_ALL) or direct message handling
void CMessanger::SendMessage(UINT message , UINT ID_WND, WPARAM wParam, LPARAM lParam)
{
for (int i = 0; i < m_aWnd.GetSize(); i++)
if ( ID_WND == IDW_ALL|ID_WND == m_aWnd.GetAt(i)->ID )
m_aWnd.GetAt(i)->m_pMsgItem->SendMessage(message, wParam, lParam);
}
//-------------------------------------------------------------
// special message handling
void CMessanger::EnableControls(bool Enable, UINT ID_WND)
{
for (int i = 0; i < m_aWnd.GetSize(); i++)
if ( ID_WND == IDW_ALL|ID_WND == m_aWnd.GetAt(i)->ID )
m_aWnd.GetAt(i)->m_pMsgItem->EnableControls( Enable );
}
//-------------------------------------------------------------
void CMessanger::ClearControls( UINT ID_WND )
{
for (int i = 0; i < m_aWnd.GetSize(); i++)
if ( ID_WND == IDW_ALL|ID_WND == m_aWnd.GetAt(i)->ID )
m_aWnd.GetAt(i)->m_pMsgItem->ClearControls();
}
//-------------------------------------------------------------
Как видно, класс CMsgItem представляет некий почти «абстрактный» командный интерфейс, и состоит только из виртуальных методов и дополнительной функции Register (одноимённая функция есть у CMessanger), которая введена косметически, для удобства регистрации. Здесь перечисляются все команды, которые необходимо выполнять согласно введённому раннее enum app_state. Выше для примера приведены только два метода. Дополнительно оставлен метод SendMessage, который теперь не имеет ничего общего с MFC или API(одноименную функцию обрабатывает CMessanger исключительно сам). Это сделано на «всякий пожарный», если во всех классах CWnd-объектов, наследующих, от CMsgItem, приемлемо ограничиться обработкой сообщений-команд только в SendMessage, например, в свиче, вместо переопределения большого числа мембер-функции EnableControls, ReadControls, и т.д в классе, derived от CMsgItem . Если кому-то нравится это, то последние можно удалить из определения CMsgItem и CMessanger. Кстати, может стоит это сделать? Последнее, что занудно уточню, и что и так всем понятно - методы CMsgItem никогда не вызываются напрямую при передаче сообщений. Это делает CMessanger (выступая от имени того, кто вызвал его соответствующий метод), вызывая переопределённый метод derived CWnd-класса(кого здесь стошнило – извините).
В классе CMessanger осталось всё без изменений, за исключение тех, что связаны с появлением на свет CMsgItem, и GetStatus(void), SetStatus(UINT _status). Об этих двух методах, как и о набивших всем оскомину функциях FillControls/DisplayControls – чуть ниже в резюме. Кроме того, в CWndInfo указатель на оконный объект, который пока И НЕ НУЖЕН, переделан на void*. Если потребуется этот CWnd-объект (а так, в принципе любого класса), то для использования его методов в теле FillControls/DisplayControls можно вызвать немного переделанную функцию GetWnd.
Как этим всем пользоваться? Например, если диалог CMainDialog будем наследовать и от CMsgItem, то так:
///////////////////////////////////////////////////////////////////
//
// CMainDialog dialog declaration
//
///////////////////////////////////////////////////////////////////
class CMainDialog : public CDialog, CMsgItem
{
// CDialog, CMainDialog declarations
…
public:
//////////////////////////////////////////////////
// CMsgItem interface general method
void SendMessage(UINT message, WPARAM wParamMsg, LPARAM lParam)
{
switch (message)
{
case UM_READ:
// Do something
break;
case UM_ERASE:
// Do something
break;
};
}
// CMsgItem interface special methods
void EnableControls(bool Enable)
{
// Do something
}
void ClearControls (void)
{
// Do something
}
…
};
///////////////////////////////////////////////////////////////////////
//
// CMainDialog dialog implementation
//
///////////////////////////////////////////////////////////////////////
//--------------------------------------------------------------------
BOOL CMainDialog::OnInitDialog()
{
CDialog::OnInitDialog();
// Inherited CMsgItem metod
Register(&theMsg, this, IDW_MAIN_DIALOG);
…
return TRUE;
}
//--------------------------------------------------------------------
void CMainDialog::OnBnClickedEditButton()
{
// «одним махом», широковещательной рассылкой очищаем все контролы
// тех объектов, о которых известно theMsg
theMsg.ClearControls();
}
//--------------------------------------------------------------------
В приведённом выше коде не хватает некого метода CMessanger – UnRegister. Не то, что бы я его не показал на форуме. Я просто про него забыл!!! из-за своего балбесянства. Последствия, которые могут быть, ясны – крах. Про него мне подсказал пост одного из участников форумов. Ниже, без персоналий привожу ЕГО фрагмент кода, где такой метод void CMediator::UnregisterRecipient(CCommandInterface *Rec) присутствует. Для тех, кто заинтересовался, и кто ещё не смотрел подобную проблематику, ооооочень рекомендую посмотреть код ЭТОГО человека. Прошу модераторов простить за перепост. Там же комментарии. Код чище, стильнее, чем у меня. Так, что некоторые вещи своего «рабочего» класса CMessanger я наглым образом перепишу, как У НЕГО:
//--------------------------------------------------------------------
//H-файл
class CCommandInterface // у меня это CMsgItem
{
public:
virtual void Execute(CCommand *cmd) = 0;
};
class CMediator // у меня это CMessanger
{
public:
static CMediator* Instance();
void RegisterRecipient(CCommandInterface *newRec);
void UnregisterRecipient(CCommandInterface *Rec);
void SendMessage(CCommand *cmd);
void SendMessage(CCommandInterface *whom, CCommand *cmd);
protected:
CMediator();
private:
static CMediator* m_instance;
CArray<CCommandInterface*, CCommandInterface*> m_recipients;
};
//--------------------------------------------------------------------
//CPP-файл
CMediator* CMediator::m_instance = NULL;
CMediator::CMediator()
{
}
CMediator* CMediator::Instance()
{
if (m_instance == NULL)
{
m_instance = new CMediator;
ASSERT(m_instance);
}
return m_instance;
}
void CMediator::RegisterRecipient(CCommandInterface *newRec)
{
for (int i = 0; i < m_recipients.GetSize(); i++)
{
if (newRec == m_recipients[i])
{
return;
}
}
m_recipients.Add(newRec);
}
void CMediator::UnregisterRecipient(CCommandInterface *Rec)
{
for (int i = 0; i < m_recipients.GetSize(); i++)
{
if (Rec == m_recipients[i])
{
m_recipients.RemoveAt(i);
return;
}
}
}
void CMediator::SendMessage(CCommand *cmd)
{
for (int i = 0; i < m_recipients.GetSize(); i++)
{
m_recipients[i]->Execute(cmd);
}
}
void CMediator::SendMessage(CCommandInterface *whom, CCommand *cmd)
{
for (int i = 0; i < m_recipients.GetSize(); i++)
{
if (whom == m_recipients[i])
{
m_recipients[i]->Execute(cmd);
}
}
}
//--------------------------------------------------------------------
//CPP- где что-то делаем
любое заинтересованное окно получает указатель на медиатор таким и только таким образом (конструктор медиатора защищенный):
CMediator *mediator = CMediator::Instance();
и регистрирует себя, скажем в своем конструкторе
mediator->RegisterRecipient((CCommandInterface*)this);
В деструкторе, соответственно разрегистрирует
CMediator *mediator = CMediator::Instance();
mediator->UnregisterRecipient((CCommandInterface*)this);
//--------------------------------------------------------------------
Человек, который написал этот код, не участвует в данном форуме. Поэтому любая критика в его адрес была бы неуместной. Это моя просьба НЕ к тем, кто уже участвует в топике данного форума, т.к. это люди воспитанные и интеллигентные. В части построения GUI - критика тоже была бы не уместна, поскольку человек решает несколько иные задачи. Насколько я понял, это те задачи, для которых собственно и были придуманы паттерны mediator и observer. Так, что этот человек НЕ ЯВЛЯЕТСЯ ДАЖЕ МОИМ СТОРОННИКОМ. Он как бы сказал: «Посмотри. Может сгодится…». Мне сгодилось.
*РЕЗЮМЕ И ДОПОЛНЕНИЯ К ШАГ 5**
1) Можно посылать широковещательные сообщения всем объектам сразу, или в отдельности при помощи медиатора CMessanger, выполняя рассылку без использования SendMessage MFC или API.
2) То, что нет SendMessage MFC или API – это хорошо. Не сложно увидеть, что наряду с CWnd–объектами (derived видов и диалогов и т.д.), для которых выше я и «шагал», CMessanger умеет рассылать команды о состояниях enum app_states ЛЮБЫМ объектам. Создайте не CWnd-класс, например, некий CDatabaseOperations, который будет наследовать от CMgItem, и он тоже может быть включён в общую цепочку обработки сообщений.
3) В CMessanger оставлены виртуальные методы FillControls/DisplayControls. Простая широковещательная рассылка в ряде случаев может привести к «нежелательным эффектам», т.к. иногда необходимо контролировать ПОСЛЕДОВАТЕЛЬНОСТЬ, в которой объекты-подписчики будут выполнять команды CMessanger и РЕЗУЛЬТАТЫ этой обработки. Посредник CMessanger - совсем простой и нечего такого не умеет делать. Приведу цитату общего характера относительно паттерна Observer, где тоже присутствует броадкаст:
«…
неожиданные обновления. Поскольку наблюдатели не располагают информацией друг о друге, им неизвестно и о том, во что обходится изменение субъекта. Безобидная, на первый взгляд, операция над субъектом может вызвать целый ряд обновлений наблюдателей и зависящих от них объектов. Более того, нечетко определенные или плохо поддерживаемые критерии зависимости могут стать причиной непредвиденных обновлений, отследить которые очень сложно.
Эта проблема усугубляется еще и тем, что простой протокол обновления не содержит никаких сведений о том, что именно изменилось в субъекте. Без дополнительного протокола, помогающего выяснить характер изменений, наблюдатели будут вынуждены проделать сложную работу для косвенного получения такой информации.
…»
Бороться с негативом ясно, как. Есть следующие возможности:
А) для бродкаст-манагера «моделировать» асинхронную обработку подписчиками. Сложность решения возрастает неимоверно.
Б) делать умных подписчисиков, запрашивающих стэйт и другое, пока даже не знаю, что…
В) дробить сообщения на более мелкие, например, UM_READ - на UM_READ и UM_XXX_READ, что также не универсально и может перегружать логику приложения событиями
Г) делать что-нибудь нууууууууууу, совсем уж простое. Это я и делаю, сохраняя FillControls и DisplayControls в качестве виртуальных методов «медиатора». Как можно использовать FillControls и DisplayControls для группы подписчиков??? тоже понятно – если можно, то обработать мессагу бродкастом. Если необходим анализ очерёдности, или ещё чего (о чём можно знать только реализуя это в дочке CMessanger конкретного проекта) – то сделаем это. Короче говоря, FillControls и DisplayControls – это дополнительные посредники, вернее дополнительное средство анализа в посреднике (медиаторе). Если так не делать, то прямая дорога к п. А)Б)В), имхо. Например, можно было бы делать такой вызов theMsg.FillControls(UM_READ_DB):
//---------------------------------------------
// h-file
class CMyMsg: public CMessanger
{
public:
bool FillControls (UINT message);
};
extern CMyMsg theMsg;
//----------------------------------------------------
// cpp-file
bool CMyMsg::FillControls(UINT message)
{
switch (message)
{
case UM_ERASE:
ClearControls();
…
…
break;
case UM_READ_DB:
FillControls(UM_ERASE);
if( GetStatus() == ST_XXXX )
((Cxxx*) GetWnd(IDW_XXXX))->XXXX();
…
…
break;
}
return true;
}
//----------------------------------------------------
PS.
Ещё раз благодарю всех тех, кто откликнулся. Уже не обещаю, что дальше не буду писать столь пространно и столь длинные посты, т.к., думаю сделать завтра последний пост типа «ИТОГИ. КАК БЫТЬ?».
To amirul
amirul, там я хотел бы сделать перепост одного хорошего фрагмента твоего ответа мне. Этика, понимаш… Буду писать только хорошее. Если не возражаешь, то я сделаю это, поскольку «колеблющиеся в ООП» могли пропустить ЭТО между прочим.
|