我正在开发一个C ++通信库,在其中我从许多设备(网络套接字,uart / usb,CAN和LIN网络)接收序列化数据。我还需要从我的消息对象中创建序列化数据。
我有一个名为MessageBase的基类,我现在有两个名为Message和CtrlMessage的派生类。该项目最终将在未来需要更多的消息类型,因此我希望使用一种设计模式来实现,以便将来轻松扩展到新的消息类型。
我的另一个目标是,正如Scott Meyes所说,难以正确使用这些类并且易于正确使用。
我开始考虑使用NVI模式并使用C ++工厂来创建消息,但是,Factory类需要处理一些标头的反序列化,以便找出有效负载中的消息类型。
class MessageBase
{
private:
// other public & private methods omitted for brevity
MessageBase &ISerialize( dsStream<byte> &sdata) = 0;
public:
MessageBase &Serialize( dsStream<byte> &sdata)
{
ISerialize(sdata);
}
}
class Message : public MessageBase
{
private:
// other public & private methods omitted for brevity
MessageBase &ISerialize( dsStream<byte> &sdata);
public:
}
class MessageFactory
{
private:
public:
CreateMessageFromStream( dsStream<byte> &RxData)
{
// read N bytes from RxData to determine type and then
// jump into switch to build message
switch(MsgType)
{
case MSG_DATA:
{
Message *pMsg = new Message(RxData);
}
break;
case MSG_CTRL:
{
MessageCtrl *pMsg = new MessageCtrl(RxData);
}
break;
}
}
// I shorten quite a bit of this to, hopefully, give the basic idea.
我一直在研究的另一种方法是Scott Meyers在他的更有效的C ++书中第33项中概述的Double Dispatch。但这似乎只是将问题转移到需要所有兄弟派生类来了解彼此或使用stl map来模拟vtable的更高级解决方案。该代码看起来很糟糕,难以理解。
我看了一下C ++访问者模式和Builder Creational模式,这些都需要调用者知道你想要提前实例化的派生类型。
我知道我可以在MessageFactory中放一个大的switch语句,如图所示,并完成它,但我所追求的是一种添加从MessageBase派生的新消息类型的方法,而不必触及MessageFactory类。我不希望其他程序员必须知道或找到所有需要为新消息类型更新代码的地方。
此外,这是一个嵌入式应用程序,在某些情况下,某些事情已经摆脱桌面。我可以使用模板编程技术,但我没有任何STL库,也没有任何Boost库支持。
有什么建议吗?
答案 0 :(得分:1)
我不知道这是否值得。但是可以写出你想做的事情的机器。
我们从MessageBase
开始。它有一个private
构造函数。
然后告诉它让MessageHelper<T>
成为friend
类。
MessageHelper
看起来像这样:
enum MessageType {
TYPE1, // notice no assigment
TYPE2, // values should be consecutive, distinct, and start at `0`
TYPE3, // or things go poorly later on.
NUM_TYPES /* should be last */
};
template<MessageType> struct MessageTag {}; // empty, for overloading
template<MessageType...> struct MessageTags {};
template<MessageType Last, MessageType... List> struct MakeMessageTags:
MakeMessageTags<MessageType(Last-1), MessageType(Last-1), List...>
{};
template<MessageType... List> struct MakeMessageTags<MessageType(0), List...>:
MessageTags<List...>
{};
typedef MessageBase*(*MessageCreatorFunc)(dsStream<byte>&);
// write this somewhere, next to a given type. If you don't, code later will fail to compile
// (yay). You could make a macro to write these:
MessageCreatorFunc MessageCreator( MessageTag<TYPE1> ) {
return []( dsStream<byte>& st )->MessageBase* {
return new MessageType1(st);
};
}
// manual compile time switch:
template<MessageType... List>
MessageBase* CreateMessageFromStream_helper( MessageType idx, dsStream<byte>& st, MessageTags<List...> )
{
static MessageCreatorFunc creator[] = { MessageCreator(MessageTag<List>())... };
return creator[idx]( st );
}
MessageBase* CreateMessageFromStream( dsStream<byte>& st ) {
// stuff, extract MessageType type
MessageBase* msg = CreateMessageFromStream_helper( type, st, MakeMessageTags<MessageType::NUM_TYPES>() );
// continue
}
上述代码的效果是我们自动构建一个手动跳转表来创建我们的消息。
如果没有人写MessageCreator( MessageTag<TYPE> )
重载,或者它在_helper
的上下文中不可见,则上述编译失败。因此,这可以确保在添加新消息类型时,您可以编写创建代码,也可以中断构建。比隐藏在某处的switch语句要好得多。
在某些地方,MessageType
和应该创建的C ++类型之间必须存在关联:上述机制只是确保如果没有设置该关联,我们就会遇到编译器错误。
您可以获得更多乐趣并获得更好的消息,而不是在MessageCreator上重载,您需要专注:
template<MessageType TYPE>
void MessageCreator( MessageTag<TYPE> ) {
static_assert( "You have failed to create a MessageCreator for a type" );
}
// specialization:
template<>
MessageCreatorFunc MessageCreator( MessageTag<TYPE1> ) {
return []( dsStream<byte>& st )->MessageBase* {
return new MessageType1(st);
};
}
这有点迟钝,但可能会生成更好的错误消息。 (虽然所有情况都不需要template<>
,因为覆盖也会替换template
,标准至少一个这样的特殊化可以编译必须存在,或者程序生病了,不需要诊断(!?))。