Deserializing unknown inherited type [C++]
Lets say I have a class which routes messages to their handlers. This class is getting the messages from another class who gets the messages through socket. So, the socket gets a buffer containing some sort of message.
The class that routes the messages is aware of the message types. Every message is inheriting Message class which contains a message ID, and of cours开发者_StackOverflow中文版e adds paraemters of it's own.
The problem is, how can I transfer the message from the buffer to be an actucal message instance of the correct type?
For exmaple, I have a DoSomethingMessage that inherites Message. I get the buffer containing the message, but I need somehow to convert the buffer back to DoSomethingMessage, without really knowing it's a DoSomethingMessage.
I could have transfer the buffer to the MessageRouter, and there check by the ID and create the right instance, but it's seemes like a really bad design to me.
Any suggestions?
You could abstract the message deserialization. Have a "MessageHolder" class that just has the buffer to the object initially. It would have a method:
IMessageInterface NarrowToInterface(MessageId id);
It wasn't clear to me if your router would already know what type of message it was or not. If it does, then it would receive the messageholder instance and call the NarrowToInterface method on it.
It would pass the id of the appropriate type. If the router didn't know what type it was, then you'd also have a property on the MessageHolder object:
MessageId GetMessageType();
that the router would use to learn what message type it was to decide where to route it. More on how that is implemented later.
The IMessageInterface is an abstract class or interface that the recipient of the message would down-cast to the appropriate type, since it would know what type to expect. If all of the different messages are well-known and you have generics or templates available to you, you could have the NarrowToInterface method be a template method that took the return value as a template parameter, so that you would have better type safety. If you don't have templates, you could use the double-dispatch technique of the "Vistor" pattern. Google "double-dispatch visitor" for more info.
If the types of messages is not well-defined or could grow in the future, you'll just have to live with a (compiler-unverifiable) downcast at some point. The implementation I'm suggesting encapsulates this as much as possible and limits coupling to its absolute minimum, as far as I know.
Also, for this to work your messages have to be framed with a standard identifier in the header. i.e. there is a standard header that has the length of the entire message as well as the ID of the message type. That way the socket endpoint can parse the basics of the message and put it into the messageholder. The MessageHolder can either know about all the different messages types itself to implement the NarrowToInterface() method or there could be a global repository that would return an "IMessageDeserializer" objects to implement NarrowToInterface for each message Type. All of the loaded message clients would register all of the deserializers for all of the messages they support with the repository and also register the message type IDs that they want with the message router.
If you're passing the message through a socket you need to pass some tag that will identify the kind of message you're passing. This way when you read the data from the socket, you know what type of object you need to create. You're code has to know what kind of message it needs to create. The binary blobs that comes from the socket contains no information about what it is.
How can you convert any data to a logical representation when you don't know what the data intends to represent in the first place? If I send you 0x2FD483EB
there's no way for you to know what it means unless you know what I intend to represent with it - maybe it's a single 32-bit number, maybe a pair of 16-bit numbers, perhaps a string of 4 8-bit characters.
Since you're getting raw data from a socket, you can't rely on the compiler magic used for polymorphism. All you can do is read the ID and create the appropriate class using good old switch
. You can, of course, wrap this in a nice object oriented layer making child classes responsible for recognizing their own ID and a factory class to create the appropriate class.
As already mentioned, you have to somehow have a mapping from an id to the corresponding type.
For a managable number of message types you can just use a central factory that creates the correct message instance for the id stored in the binary message (e.g. using something like switch(messageId)
).
My guess though is you're mostly worried about the centralisation of a giant factory method, i.e. if the number of messages becomes big. If you want to decentralize that you need to somehow register your classes, see e.g. this answer for the basic idea.
Use this approach to register a factory for your subclass with a central registrar. E.g.:
// common code:
struct MessageBase {
virtual ~MessageBase() {}
};
typedef MessageBase* (*MessageConstructor)(char* data);
struct MessageRegistrar {
static std::map<unsigned, MessageConstructor> messages;
MessageRegistrar(unsigned id, MessageConstructor f) {
messages[id] = f;
}
static MessageBase* construct(unsigned id, char* data) {
return messages[id](data);
}
};
#define REGISTER_MESSAGE(id, f) static MessageRegistrar registration_##id(id, f);
// implementing a new message:
struct ConcreteMessage : MessageBase {
ConcreteMessage(char* data) {}
static MessageBase* construct(char* data) {
return new ConcreteMessage(data);
}
};
REGISTER_MESSAGE(MessageId_Concrete, &ConcreteMessage::construct);
// constructing instances from incoming messages:
void onIncomingData(char* buffer) {
unsigned id = getIdFromBuffer(buffer);
MessageBase* msg = MessageRestristrar::construct(id, buffer);
// ...
}
精彩评论