Question

I am new to C++ and came to a point, where I generate an overhead with classes. I have a QTcpSocket and read messages from it and create objects, for example MessageJoin, MessagePart, MessageUserData etc. I send these objects to my client and display them (+ do some UI updating).

Now here comes my problem. I tested a few design techniques but all of them are not that nice:

  • Pass each parameter of a message object in a signal/slot connection to the client - small overhead but not that good-looking
  • Create a method for each Message-Type (messageJoinReceived, messageNoticeReceived etc.)
  • Create one method and use dynamic_cast to cast für each class and test it

For a better understanding, I added my dynamic_cast version. As a said, the code looks ugly and unusable. My questions are:

  • Is there a better way to do it with (a) dynamic_cast
  • Is there another way (For example a design pattern) to solve such a problem ? maybe add a method in the classes and return the type or something like this
  • I read about the visitor pattern. This pattern is just for dynamic object types in Getter/Setter methods ?

A few side notes

  • I can use RTTI
  • Speed isn't a big deal. Clean and understandable code is more important
  • I use Qt and have the possiblity to use qobject_cast and signal/slots

Here is my code (Pastebin-Link):

// Default class - contains the complete message (untouched)
class Message
{
public:
    QString virtual getRawMessage() { return dataRawMessage; }
protected:
    QString dataRawMessage;
};

// Join class - cointains the name of the joined user and the channel
class MessageJoin : public Message
{
public:
    MessageJoin(const QString &rawmessage, const QString &channel, const QString &user)
    {
        dataRawMessage = rawmessage;
        dataChannel = channel;
        dataUser = user;
    }

    QString getChannel() { return dataChannel; }
    QString getUser(){ return dataUser; }

private:
    QString dataChannel;
    QString dataUser;
};

// Notice class - contains a notification message
class MessageNotice : public Message
{
public:
    MessageNotice(const QString &rawmessage, const QString &text)
    {
        dataRawMessage = rawmessage;
        dataText = text;
    }

    QString getText() { return dataText;}

private:
    QString dataText;
};

// Client code - print message and update UI
void Client::messageReceived(Message *message)
{
    if(message)
    {
        MessageJoin *messagejoin;
        MessagePart *messagepart;
        MessageNotice *messagenotice;
        if((messagejoin = dynamic_cast<MessageJoin *>(message)) != 0)
        {
            qDebug() << messagejoin->getUser() << " joined " << messagejoin->getChannel();
            // Update UI: Add user
        }
        else if((messagenotice = dynamic_cast<MessageNotice *>(message)) != 0)
        {
            qDebug() << messagenotice->getText();
            // Update UI: Display message
        }
        else
        {
            qDebug() << "Cannot cast message object";
        }
        delete message; // Message was allocated in the library and is not used anymore
    }
}
Was it helpful?

Solution

This looks quite similar to the expression problem and AFAIK there is no way to avoid casts if you are going to add new messages and new ways to handle them. However it's not that hard to make more eye pleasing wrap for necessary run-time stuff. Just create a map from message type to corresponding handler using typeid.


#include <functional>
#include <typeindex>
#include <typeinfo>
#include <unordered_map>

typedef std::function<void(Message *)> handler_t;

typedef std::unordered_map<
    std::type_index,
    handler_t> handlers_map_t;

template <class T, class HandlerType>
handler_t make_handler(HandlerType handler)
{
    return [=] (Message *message) { handler(static_cast<T *>(message)); };
}

template <class T, class HandlerType>
void register_handler(
    handlers_map_t &handlers_map,
    HandlerType handler)
{
    handlers_map[typeid(T)] = make_handler<T>(handler);
}

void handle(handlers_map_t const &handlers_map, Base *message)
{
    handlers_map_t::const_iterator i = handlers_map.find(typeid(*message));
    if (i != handlers_map.end())
    {
        (i->second)(message);
    }
    else
    {
        qDebug() << "Cannot handle message object";
    }
}

Then register handlers for specific message types:


handlers_map_t handlers_map;

register_handler<MessageJoin>(
    handlers_map,
    [] (MessageJoin  *message)
    {
        qDebug() << message->getUser() << " joined " << message->getChannel();
        // Update UI: Add user
    });

register_handler<MessageNotice>(
    handlers_map,
    [] (MessageNotice *message)
    {
        qDebug() << message->getText();
        // Update UI: Display message
    });

And now you can handle messages:


// simple test
Message* messages[] =
{
    new MessageJoin(...),
    new MessageNotice(...),
    new MessageNotice(...),
    new MessagePart(...),
};

for (auto m: messages)
{
    handle(handlers_map, m);
    delete m;
}

Surely you might want to make some improvements like wrapping handlers stuff into reusable class, using QT or boost signals/slots so you can have multiple handlers for a single message, but the core idea is the same.

OTHER TIPS

The visitor pattern could be a good fit i.e.

class Message
{
public:
    QString virtual getRawMessage() { return dataRawMessage; }

    virtual void accept(Client& visitor) = 0;

protected:
    QString dataRawMessage;
};

// Join class - cointains the name of the joined user and the channel
class MessageJoin : public Message
{
public:
    MessageJoin(const QString &rawmessage, const QString &channel, const QString &user)
    {
        dataRawMessage = rawmessage;
        dataChannel = channel;
        dataUser = user;
    }

    QString getChannel() { return dataChannel; }
    QString getUser(){ return dataUser; }

    void accept(Client& visitor) override
    {
          visitor.visit(*this);
    }

private:
    QString dataChannel;
    QString dataUser;
};

// Notice class - contains a notification message
class MessageNotice : public Message
{
public:
    MessageNotice(const QString &rawmessage, const QString &text)
    {
        dataRawMessage = rawmessage;
        dataText = text;
    }

    QString getText() { return dataText;}

    void accept(Client& visitor) override
    {
          visitor.visit(*this);
    }

private:
    QString dataText;
};

void Client::visit(MessageJoin& msg)
{
    qDebug() << msg.getUser() << " joined " << msg.getChannel();
    // Update UI: Add user
}

void Client::visit(MessageNotice& msg)
{
    qDebug() << msg.getText();
    // Update UI: Display message
}

// Client code - print message and update UI
void Client::messageReceived(Message *message)
{
    if(message)
    {
        message->visit(this);
        delete message; // Message was allocated in the library and is not used anymore
    }
}

A better design might be to have an abstract virtual function in the Message class, called process or onReceive or similar, the sub-classes implements this function. Then in Client::messageReceived just call this function:

message->onReceive(...);

No need to for the dynamic_cast.

I would also recommend you to look into smart pointers, such as std::unique_ptr.


If you have private data in the Client class that is needed for the message processing functions, then there are many methods of solving that:

  • The simplest is to use a plain "getter" function in the client:

    class Client
    {
    public:
        const QList<QString>& getList() const { return listContainingUiRelatedStuff; }
        // Add non-const version if you need to modify the list
    };
    
  • If you just want add items to the list in your example, then add a function for that:

    void addStringToList(const QString& str)
        { listContainingUiRelatedStuff.push_back(str); }
    
  • Or the non-recommended variant, make Client a friend in all message classes.

The second variant is what I recommend. For example, if you have a list of all connected clients and want to send a message to all of them, then create a function sendAll that does it.

The big idea here is to try and minimize the coupling and dependencies between your classes. The less coupling there is, the easier it will be to modify one or the other, or add new message classes, or even completely rewrite one or the other of the involved classes without it affecting the other classes. This is why we split code into interface and implementation and data hiding.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top