Possible connector design pattern in c++?
Recently I was working on an AI assignment and I needed a method to pass around notification which would cause the receiving objects to act upon it. Problem however was that none of the objects knew each other. I had many AI modules which were all split up (for example driving to a destination would exist out of destination chooser, path-finding algorithm, obstruction evader , driver mechanism, etc) and not all of them needed to concern themselves with notifications (a notification would be “change destination”) and often they only cared about a single notification.
As long as the AI stays simple, you are good with a few notification (in fact I think I only needed two) but as I was writing it I noticed that adding another notification would require changing 4 different classes. If I would heavily rely on my notification passing system I would have been in big trouble.
There are a few ways to make the system simpler and the first one would be that there was only one notification which hold all possible data a notification could have and an ID which would help figure out what the type of message it was. The downside of this method I would need to check the ID and then act upon it. This was something I did not like.
Another method would be to have all AI components derive from a base class which had all the receiving notification messages ( OnChangeTarget(int targetID); ). This was the method I choose in the end (as I started to get me more and more off track while I should be working on the assignment) and since I need only two functions (remember, two messages) it wasn’t that hard . However I promised myself that after delivering the assignment I would find a way to improve this.
And this morning while I was walking the dog something suddenly hit me. Since neither type can recognize the other one (they both start as abstract classes) I need to find a way to connect the types. I had thought of this option before, but since the connecting method would need to be able to handle abstract types it should be abstract as well.
Since I'm assume this has become quite confusing so far I will post the code that demonstrate what I want to do.
//////////////////////////////////////////////////////////////////////////
// Two different notifications
struct HelloNote : public INotification {};
struct WorldNote : public INotification {};
// The receivers
// ...
//
int main ()
{
INotification* Note1 = new HelloNote();
INotification* Note2 = new WorldNote();
INotification* AllNotes[2] = {Note1, Note2};
IReceiver* HelloRCV = new HelloReceiver();
IReceiver* WorldRCV = new WorldReceiver();
IReceiver* HelWoRCV = new HelloWorldReceiver();
IReceiver* AllReceivers[3] = {HelloRCV, WorldRCV, HelWoRCV};
for(int i = 0; i < 2; i++)
{
for(int j = 0; j < 3; j++)
{
AllReceivers[j]->InitialReceive(AllNotes[i]);
}
}
// Removing all types types
delete Note1;
delete Note2;
delete HelloRCV;
delete WorldRCV;
delete HelWoRCV;
return 0;
}
As you noticed there is no coded type identification within the message (in fact except their names they are identical) and because the receivers are downcasted as well you can no longer see which type should recieve what kind of message. However after we run the final version of the exact same code (I promise I won’t change main at all) the classes will recieve the correct messages.
I’m not certain if the following method is an existing design pattern (believe me, I looked for it). But it is similar to the mediator pattern, but were the mediator pattern acts between two different set of variables, I'm acting between two different types. The solution I provide mediates based on type and not on instance. For now I think the description of an "abstract connector" would be better, but since I'm not 100% certain I will leave it at "possible".
Although I Intended to explain it I decided against it, since the code looks simple (as it is in fact), but it will be faster to experience and then explain it than the otherway around.
#include <iostream>
#include <typeinfo> //for 'typeid' to work
#include <list>
//-------------------------------------------------------------------------------------
// Library code
// You can copy this straight in your code and it will never have a need for
// modification.
//-------------------------------------------------------------------------------------
struct INotification{
virtual ~INotification(){};
};
class IReceiver;
struct BaseConnector
{
public:
virtual void Connect(IReceiver* aRCV, INotification* aNote) = 0;
};
struct Translator
{
const char* m_Type;
BaseConnector* m_Interupter;
Translator(const char* type, BaseConnector* inter) :
m_Type(type), m_Interupter(inter)
{}
};
typedef std::list<Translator> TranslationList;
class IReceiver
{
protected:
TranslationList m_Translators;
public:
void InitialReceive(INotification* aNote) {
TranslationList::iterator begin = m_Translators.begin();
while(begin!=m_Translators.end())
{
if(strcmp((*begin).m_Type,(typeid(*aNote).raw_name())) == 0)
{
(*begin).m_Interupter->Connect(this, aNote);
}
begin++;
}
}
void Receive(INotification* aNote) { std::cout << "Unhandled\n"; }
};
template <typename R, typename N>
struct TConnector : public BaseConnector
{
public:
void Connect(IReceiver* aRCV, INotification* aNote)
{
((R*)aRCV)->Receive((N*)(aNote));
}
};
template <typename R, typename N>
struct CTranslator : public Translator
{
CTranslator() : Translator(typeid(N).raw_name(), new TConnector<R, N>) {}
};
//-------------------------------------------------------------------------------------
// User code
// This is what you will be writing.
//-------------------------------------------------------------------------------------
//////////////////////////////////////////////////////////////////////////
// Two different notifications
struct HelloNote : public INotification {};
struct WorldNote : public INotification {};
//////////////////////////////////////////////////////////////////////////
// Our Hello Reciever, only accepts HelloNotes
class HelloReceiver : public IReceiver
{
public:
HelloReceiver()
{
m_Translators.push_back(CTranslator<HelloReceiver, HelloNote>() );
}
void Receive(HelloNote* aHello) {
std::cout << " " << __FUNCTION__ << "(HelloNote* aHello)\n";
}
};
//////////////////////////////////////////////////////////////////////////
// Our world Reciever, only accepts WorldNotes
class WorldReceiver : public IReceiver
{
public:
WorldReceiver()
{
m_Translators.push_back(CTranslator<WorldReceiver, WorldNote>() );
}
void Receive(WorldNote* aWorld) {
std::cout << " " << __FUNCTION__ << "(WorldNote* aWorld)\n"
}
};
//////////////////////////////////////////////////////////////////////////
// Our HelloWorld Reciever, accepts both messages
class HelloWorldReceiver : public IReceiver
{
public:
HelloWorldReceiver()
{
m_Translators.push_back( CTranslator<HelloWorldReceiver, WorldNote>() );
m_Translators.push_back( CTranslator<HelloWorldReceiver, HelloNote>() );
}
void Receive(WorldNote* aWorld) {
std::cout << " " << __FUNCTION__ << "(WorldNote* aWorld)\n";
}
void Receive(HelloNote* aHello) {
std::cout << " " << __FUNCTION__ << "(HelloNote* aHello)\n";
}
};
int main ()
{
INotification* Note1 = new HelloNote();
INotification* Note2 = new WorldNote();
INotification* AllNotes[2] = {Note1, Note2};
IReceiver* HelloRCV = new HelloReceiver();
IReceiver* WorldRCV = new WorldReceiver();
IReceiver* HelWoRCV = new HelloWorldReceiver();
IReceiver* AllReceivers[3] = {HelloRCV, WorldRCV, HelWoRCV};
for(int i = 0; i < 2; i++)
{
for(int j = 0; j < 3; j++)
{
AllReceivers[j]->InitialReceive(AllNotes[i]);
}
}
// Removing all types types
delete Note1;
delete Note2;
delete HelloRCV;
delete WorldRCV;
delete HelWoRCV;
return 0;
}<
If you would look at TConnector you will notice that the virtual function (connect) has an implementation which based on the design casts the receiver and the notification to the correct type.
If now you would look at IReceiver you will see that the InitialRecieve function uses typeid to recover the type information and checks it against an internal list to see if we have a connection for it. Since by dereferencing we will find out the original type we will work with a known type.
Now look at the receiver classes. You see that in each receiver class we need to store a connection. We have to define a connection and the corresponding function. If the connection is missing the data will not be send to their respective receive function. If the function is missing you will get an error.
There are also many things that could still be done to improve it
Also attached to the article you will find the entire solution inside a zip file: PatternDesign Connector (4kb, source)